diff --git a/smart-hut/src/components/AutomationModal.js b/smart-hut/src/components/AutomationModal.js new file mode 100644 index 0000000..626a6c5 --- /dev/null +++ b/smart-hut/src/components/AutomationModal.js @@ -0,0 +1,190 @@ +import React, { Component } from "react"; +import { Button, Header, Modal, Icon, Responsive } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../remote"; +import { appActions } from "../storeActions"; +//import { update } from "immutability-helper"; + +class AutomationModal extends Component { + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + + this.addAutomationModal = this.addAutomationModal.bind(this); + this.modifyAutomationModal = this.modifyAutomationModal.bind(this); + this.deleteAutomation = this.deleteAutomation.bind(this); + } + + get initialState() { + return { + //INITIAL STATE HERE + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get type() { + return !this.props.id ? "new" : "modify"; + } + + addAutomationModal = (e) => { + /*let data = { + // DATA HERE + };*/ + // TODO CALL TO REMOTE SERVER TO ADD SCENE + /*this.props + .saveRoom(data, null) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in creating room", err));*/ + }; + + modifyAutomationModal = (e) => { + /* let data = { + // DATA HERE + };*/ + // TODO CALL TO REMOTE SERVER TO MODIFY SCENE + /*this.props + .saveRoom(data, this.props.id) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in updating room", err));*/ + }; + + deleteAutomation = (e) => { + // TODO CALL TO REMOTE SERVER TO DELETE SCENE + /* + this.props + .deleteRoom(this.props.id) + .then(() => this.closeModal()) + .catch((err) => console.error("error in deleting room", err));*/ + }; + + changeSomething = (event) => { + let nam = event.target.name; + let val = event.target.value; + this.setState({ [nam]: val }); + }; + + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; + + render() { + return ( +
+ {!this.props.nicolaStop ? ( +
+ + {this.type === "new" ? ( + + ) : ( + + )} + + + {this.type === "new" ? ( + + ) : ( + + )} + +
+ ) : null} + + +
+ {this.type === "new" ? "Add new automation" : "Modify automation"} +
+ + { + //TODO FORM TO ADD OR MODIFY SCENE + } + + {this.type === "modify" ? ( + + ) : null} + + + + + + +
+
+ ); + } +} + +const setActiveAutomation = (activeAutomation) => { + return (dispatch) => + dispatch(appActions.setActiveAutomation(activeAutomation)); +}; + +const mapStateToProps = (state, ownProps) => ({ + automations: ownProps.id ? state.automations[ownProps.id] : null, +}); +const AutomationModalContainer = connect( + mapStateToProps, + { ...RemoteService, setActiveAutomation }, + null, + { forwardRef: true } +)(AutomationModal); +export default AutomationModalContainer; diff --git a/smart-hut/src/components/HeaderController.js b/smart-hut/src/components/HeaderController.js index d6ef4c5..1d84c6b 100644 --- a/smart-hut/src/components/HeaderController.js +++ b/smart-hut/src/components/HeaderController.js @@ -57,7 +57,9 @@ export class MyHeader extends React.Component { {this.props.username} - + diff --git a/smart-hut/src/components/SceneModal.js b/smart-hut/src/components/SceneModal.js new file mode 100644 index 0000000..9183a58 --- /dev/null +++ b/smart-hut/src/components/SceneModal.js @@ -0,0 +1,204 @@ +import React, { Component } from "react"; +import { + Button, + Header, + Modal, + Icon, + Responsive, + Form, + Input, +} from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../remote"; +import { appActions } from "../storeActions"; +//import { update } from "immutability-helper"; + +class SceneModal extends Component { + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + + this.addSceneModal = this.addSceneModal.bind(this); + this.modifySceneModal = this.modifySceneModal.bind(this); + this.deleteScene = this.deleteScene.bind(this); + } + + get initialState() { + return { + name: this.type === "new" ? "New Scene" : this.props.scene.name, + openModal: false, + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get type() { + return !this.props.id ? "new" : "modify"; + } + + addSceneModal = (e) => { + let data = { + name: this.state.name, + }; + + this.props + .saveScene(data, null) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in creating room", err)); + }; + + modifySceneModal = (e) => { + let data = { + name: this.state.name, + }; + + this.props + .saveScene(data, this.props.id) + .then(() => { + this.setInitialState(); + this.closeModal(); + }) + .catch((err) => console.error("error in updating room", err)); + }; + + deleteScene = (e) => { + this.props + .deleteScene(this.props.id) + .then(() => this.closeModal()) + .catch((err) => console.error("error in deleting room", err)); + }; + + changeSomething = (event) => { + let nam = event.target.name; + let val = event.target.value; + this.setState({ [nam]: val }); + }; + + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; + + render() { + return ( +
+ {!this.props.nicolaStop ? ( +
+ + {this.type === "new" ? ( + + ) : ( + + )} + + + {this.type === "new" ? ( + + ) : ( + + )} + +
+ ) : null} + + +
+ {this.type === "new" ? "Add new scene" : "Modify scene"} +
+ +
+

Insert the name of the scene:

+ + + +
+ + {this.type === "modify" ? ( + + ) : null} +
+ + + + + +
+
+ ); + } +} + +const setActiveScene = (activeScene) => { + return (dispatch) => dispatch(appActions.setActiveScene(activeScene)); +}; + +const mapStateToProps = (state, ownProps) => ({ + scene: ownProps.id ? state.scenes[ownProps.id] : null, +}); +const SceneModalContainer = connect( + mapStateToProps, + { ...RemoteService, setActiveScene }, + null, + { forwardRef: true } +)(SceneModal); +export default SceneModalContainer; diff --git a/smart-hut/src/components/dashboard/AutomationsPanel.js b/smart-hut/src/components/dashboard/AutomationsPanel.js new file mode 100644 index 0000000..4fbf809 --- /dev/null +++ b/smart-hut/src/components/dashboard/AutomationsPanel.js @@ -0,0 +1,23 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; + +class AutomationsPanel extends Component { + constructor(props) { + super(props); + } + + render() { + return

AUTOMATIONS

; + } +} + +const mapStateToProps = (state, _) => ({ + activeRoom: state.active.activeRoom, + activeTab: state.active.activeTab, +}); +const AutomationsPanelContainer = connect( + mapStateToProps, + RemoteService +)(AutomationsPanel); +export default AutomationsPanelContainer; diff --git a/smart-hut/src/components/dashboard/DevicePanel.js b/smart-hut/src/components/dashboard/DevicePanel.js index 5e0fb17..449642e 100644 --- a/smart-hut/src/components/dashboard/DevicePanel.js +++ b/smart-hut/src/components/dashboard/DevicePanel.js @@ -2,7 +2,6 @@ import React, { Component } from "react"; import { Grid } from "semantic-ui-react"; -import { panelStyle } from "./devices/styleComponents"; import Device from "./devices/Device"; import NewDevice from "./devices/NewDevice"; import { connect } from "react-redux"; @@ -16,29 +15,29 @@ class DevicePanel extends Component { } getDevices() { - this.props - .fetchDevices() - .catch((err) => console.error(`error fetching devices:`, err)); + if (this.props.tab === "Devices") { + this.props + .fetchDevices() + .catch((err) => console.error(`error fetching devices:`, err)); + } } render() { return ( -
- - {this.props.devices.map((e, i) => { - return ( - - - - ); - })} - {!this.props.isActiveRoomHome ? ( - - + + {this.props.devices.map((e, i) => { + return ( + + - ) : null} - -
+ ); + })} + {!this.props.isActiveRoomHome ? ( + + + + ) : null} + ); } } diff --git a/smart-hut/src/components/dashboard/NewSceneDevice.js b/smart-hut/src/components/dashboard/NewSceneDevice.js new file mode 100644 index 0000000..7ea1664 --- /dev/null +++ b/smart-hut/src/components/dashboard/NewSceneDevice.js @@ -0,0 +1,142 @@ +import React, { Component } from "react"; +import { Button, Modal, Icon, Image, Form, Dropdown } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; +import styled from "styled-components"; +//import { appActions } from "../../storeActions"; + +const StyledDiv = styled.div` + background-color: #505bda; + padding: 3rem; + width: 10rem; + height: 10rem; + border-radius: 100%; + border: none; + position: relative; + box-shadow: 3px 2px 10px 5px #ccc; + transition: all 0.3s ease-out; + :hover { + background-color: #4345d9; + } + :active { + transform: translate(0.3px, 0.8px); + box-shadow: 0.5px 0.5px 7px 3.5px #ccc; + } +`; + +class NewSceneDevice extends Component { + constructor(props) { + super(props); + + this.state = { + openModal: false, + sceneDevices: this.props.scene ? this.props.scene.sceneStates : {}, + deviceName: "", + }; + this.getDevices(); + + this.setSceneState = this.setSceneState.bind(this); + this.createState = this.createState.bind(this); + } + + getDevices() { + this.props + .fetchDevices() + .catch((err) => console.error(`error fetching devices:`, err)); + } + + handleOpen = () => { + this.setState({ openModal: true }); + }; + handleClose = () => { + this.setState({ openModal: false }); + }; + + resetState = () => { + this.setState(this.baseState); + this.handleClose(); + }; + + setSceneState(e, d) { + this.setState({ devicesAttached: d.value }); + } + + createState() { + const device = this.props.devices.filter( + (e) => this.state.devicesAttached[0] === e.id + ); + let data = { + sceneId: this.props.activeScene, + id: device[0].id, + kind: device[0].kind, + }; + this.props + .saveState(data) + .catch((err) => console.error("error in creating state", err)); + this.resetState(); + } + + render() { + const availableDevices = []; + this.props.devices.forEach((e) => { + if (!Object.keys(this.state.sceneDevices).find((d) => e.id === d)) { + availableDevices.push({ + key: e.id, + text: e.name, + value: e.id, + }); + } + }); + return ( + + + + } + centered={true} + > + Add a New Scene State + +
+ + + + +
+
+ + + +
+ ); + } +} + +const mapStateToProps = (state, _) => ({ + devices: Object.values(state.devices), + activeScene: state.active.activeScene, +}); +const NewSceneDeviceContainer = connect( + mapStateToProps, + RemoteService +)(NewSceneDevice); +export default NewSceneDeviceContainer; diff --git a/smart-hut/src/components/dashboard/ScenesPanel.js b/smart-hut/src/components/dashboard/ScenesPanel.js new file mode 100644 index 0000000..2c5c726 --- /dev/null +++ b/smart-hut/src/components/dashboard/ScenesPanel.js @@ -0,0 +1,57 @@ +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { RemoteService } from "../../remote"; +import Device from "./devices/Device"; +import NewSceneDevice from "./NewSceneDevice"; +import { Grid } from "semantic-ui-react"; + +class ScenesPanel extends Component { + constructor(props) { + super(props); + } + + render() { + return ( + + {!this.props.isActiveDefaultScene + ? this.props.sceneStates.map((e, i) => { + return ( + + + + ); + }) + : null} + {!this.props.isActiveDefaultScene ? ( + + + + ) : ( + Welcome to the Scene View, you add a Scene + )} + + ); + } +} + +const mapStateToProps = (state, _) => ({ + get sceneStates() { + if (state.active.activeScene !== -1) { + const stateArray = [ + ...state.scenes[state.active.activeScene].sceneStates, + ].sort(); + return stateArray.map((id) => state.sceneStates[id]); + } else { + return []; + } + }, + get isActiveDefaultScene() { + return state.active.activeScene === -1; + }, + activeScene: state.active.activeScene, +}); +const ScenesPanelContainer = connect( + mapStateToProps, + RemoteService +)(ScenesPanel); +export default ScenesPanelContainer; diff --git a/smart-hut/src/components/dashboard/devices/Device.js b/smart-hut/src/components/dashboard/devices/Device.js index 5ee6399..d71c79e 100644 --- a/smart-hut/src/components/dashboard/devices/Device.js +++ b/smart-hut/src/components/dashboard/devices/Device.js @@ -34,19 +34,19 @@ class Device extends React.Component { console.log(this.props.device) switch (this.props.device.kind) { case "regularLight": - return ; + return ; case "sensor": - return ; + return ; case "motionSensor": - return ; + return ; case "buttonDimmer": - return ; + return ; case "knobDimmer": - return ; + return ; case "smartPlug": - return ; + return ; case "switch": - return ; + return ; case "dimmableLight": return ; case "securityCamera": @@ -61,24 +61,36 @@ class Device extends React.Component { {this.renderDeviceComponent()} - -
{this.props.device.name}
- - {this.props.device.kind === "smartPlug" ? ( + {this.props.tab === "Devices" ? ( + +
{this.props.stateOrDevice.name}
- ) : null} -
+ {this.props.stateOrDevice.kind === "smartPlug" ? ( + + ) : null} +
+ ) : ( + +
{this.props.stateOrDevice.name}
+
+ )} +
@@ -87,7 +99,14 @@ class Device extends React.Component { } const mapStateToProps = (state, ownProps) => ({ - device: state.devices[ownProps.id], + get stateOrDevice() { + if (state.active.activeTab === "Devices") { + return state.devices[ownProps.id]; + } else { + const sceneState = state.sceneStates[ownProps.id]; + return state.devices[sceneState]; + } + }, }); const DeviceContainer = connect(mapStateToProps, RemoteService)(Device); export default DeviceContainer; diff --git a/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js b/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js index fcf29a0..ebb2d91 100644 --- a/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js +++ b/smart-hut/src/components/dashboard/devices/DeviceSettingsModal.js @@ -1,10 +1,18 @@ import React, { Component, useState } from "react"; -import { Button, Form, Icon, Header, Modal } from "semantic-ui-react"; +import { Button, Form, Icon, Header, Modal, Input } from "semantic-ui-react"; import { connect } from "react-redux"; import { RemoteService } from "../../../remote"; const DeleteModal = (props) => ( - Remove} closeIcon> + + + Delete device + + } + closeIcon + >
); @@ -63,15 +72,19 @@ class DeviceSettingsModal extends Component { this.deleteDevice = this.deleteDevice.bind(this); } - openModal() { - this.setState({ open: true }); - } + closeModal = (e) => { + this.setState({ openModal: false }); + }; + + openModal = (e) => { + this.setState({ openModal: true }); + }; updateDevice(values) { if (values.name.length === 0) return; this.props .saveDevice({ ...this.props.device, name: values.name }) - .then(() => this.setState({ open: false })) + .then(() => this.setState({ openModal: false })) .catch((err) => console.error( `settings modal for device ${this.props.id} deletion error`, @@ -94,10 +107,11 @@ class DeviceSettingsModal extends Component { render() { const SettingsModal = () => ( - + Settings of {this.props.device.name} { - if (this.props.device.name.length > 15) { - return this.props.device.name.slice(0, 12) + "..."; + // setName = () => { + // if (this.props.device.name.length > 15) { + // return this.props.device.name.slice(0, 12) + "..."; + // } + // return this.props.device.name; + // }; + + componentDidUpdate(prevProps) { + if ( + this.props.device.kind === "sensor" && + this.props.device.value !== prevProps.device.value + ) { + this.setState({ value: this.props.device.value }); + } else if ( + this.props.device.kind === "motionSensor" && + this.props.device.detected !== prevProps.device.detected + ) { + this.setState({ motion: true, detected: this.props.device.detected }); } - return this.props.device.name; - }; + } componentDidMount() { if (this.props.device.kind === "sensor") { @@ -69,16 +84,19 @@ class Sensor extends Component { this.units = "ÂșC"; this.colors = temperatureSensorColors; this.icon = "temperatureIcon.svg"; + this.name = "Temperature Sensor"; break; case "HUMIDITY": this.units = "%"; this.colors = humiditySensorColors; this.icon = "humidityIcon.svg"; + this.name = "Humidity Sensor"; break; case "LIGHT": this.units = "lm"; this.colors = lightSensorColors; this.icon = "lightSensorIcon.svg"; + this.name = "Light Sensor"; break; default: this.units = ""; @@ -117,7 +135,7 @@ class Sensor extends Component { }} > - {this.props.device.name} + Motion Sensor ); @@ -162,7 +180,7 @@ class Sensor extends Component { dy="0.4em" fontWeight="bold" > - {this.setName()} ({this.props.device.id}) + {this.name} diff --git a/smart-hut/src/components/dashboard/devices/styleComponents.js b/smart-hut/src/components/dashboard/devices/styleComponents.js index 9c052f7..5c6c927 100644 --- a/smart-hut/src/components/dashboard/devices/styleComponents.js +++ b/smart-hut/src/components/dashboard/devices/styleComponents.js @@ -18,11 +18,19 @@ export const editButtonStyle = { }; export const panelStyle = { - position: "relative", backgroundColor: "#fafafa", - height: "100vh", - width: "auto", + height: "85vh", padding: "0rem 3rem", + color: "#000000", + overflow: "auto", + maxHeight: "75vh", +}; + +export const mobilePanelStyle = { + backgroundColor: "#fafafa", + minHeight: "100vh", + padding: "0rem 3rem", + color: "#000000", }; export const editModeStyle = { diff --git a/smart-hut/src/remote.js b/smart-hut/src/remote.js index 3100417..6ea0454 100644 --- a/smart-hut/src/remote.js +++ b/smart-hut/src/remote.js @@ -61,6 +61,28 @@ const Endpoint = { }); }, + /** + * Performs a non-authenticated post and put request for registration, reset-password + * @param {post} the desired method + * @param {String} route the desired route (e.g. "/rooms/1/devices") + * @param {[String]String} query query ('?') parameters (no params by default) + * @param {any} body the JSON request body + */ + sendNA: (method, route, query = {}, body = null) => { + return Endpoint.axiosInstance(route, { + method: method, + params: query, + data: ["put", "post"].indexOf(method) !== -1 ? body : null, + }).then((res) => { + if (!res.data) { + console.error("Response body is empty"); + return null; + } else { + return res; + } + }); + }, + /** * Performs login * @param {String} usernameOrEmail @@ -112,6 +134,17 @@ const Endpoint = { return this.send("post", route, query, body); }, + /** + * Performs a non-authenticated POST request + * @param {String} route the desired route (e.g. "/rooms/1/devices") + * @param {[String]String} query query ('?') parameters (no params by default) + * @param {any} body the JSON request body + * @returns {Promise<*, *>} The Axios-generated promise + */ + postNA(route, query, body) { + return this.sendNA("post", route, query, body); + }, + /** * Performs an authenticated PUT request * @param {String} route the desired route (e.g. "/rooms/1/devices") @@ -123,6 +156,17 @@ const Endpoint = { return this.send("put", route, query, body); }, + /** + * Performs a non-authenticated PUT request + * @param {String} route the desired route (e.g. "/rooms/1/devices") + * @param {[String]String} query query ('?') parameters (no params by default) + * @param {any} body the JSON request body + * @returns {Promise<*, *>} The Axios-generated promise + */ + putNA(route, query = {}, body = {}) { + return this.sendNA("put", route, query, body); + }, + /** * Performs an authenticated DELETE request * @param {get|post|put|delete} the desired method @@ -227,6 +271,23 @@ export const RemoteService = { }; }, + /** + * Fetches all scenes that belong to this user. This call does not + * populate the devices attribute in scenes. + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchAllScenes: () => { + return (dispatch) => { + return Endpoint.get("/scene") + .then((res) => void dispatch(actions.scenesUpdate(res.data))) + .catch((err) => { + console.error("Fetch all scenes error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** * Fetches all devices in a particular room, or fetches all devices. * This also updates the devices attribute on values in the map rooms. @@ -246,6 +307,25 @@ export const RemoteService = { }; }, + /** + * Fetches all devices in a particular scene, or fetches all devices. + * This also updates the devices attribute on values in the map scenes. + * @param {Number} sceneId the scene to which fetch devices + * from, null to fetch from all scenes + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + fetchStates: (sceneId) => { + return (dispatch) => { + return Endpoint.get(`/scene/${sceneId}/states`) + .then((res) => void dispatch(actions.statesUpdate(sceneId, res.data))) + .catch((err) => { + console.error(`Fetch devices sceneId=${sceneId} error`, err); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** * Creates/Updates a room with the given data * @param {String} data.name the room's name, @@ -272,6 +352,28 @@ export const RemoteService = { }; }, + /** + * Creates/Updates a scene with the given data + * @param {String} data.name the scene's name, + * @param {Number|null} sceneId the scene's id if update, null for creation + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + saveScene: (data, sceneId = null) => { + return (dispatch) => { + data = { + name: data.name, + }; + + return (sceneId + ? Endpoint.put(`/scene/${sceneId}`, {}, data) + : Endpoint.post(`/scene`, {}, data) + ) + .then((res) => void dispatch(actions.sceneSave(res.data))) + .catch(parseValidationErrors); + }; + }, + /** * Creates/Updates a device with the given data. If * data.id is truthy, then a update call is performed, @@ -303,6 +405,35 @@ export const RemoteService = { }; }, + /** + * Creates/Updates a state with the given data. If + * data.id is truthy, then a update call is performed, + * otherwise a create call is performed. The update URL + * is computed based on data.kind when data.flowType = + * 'OUTPUT', otherwise the PUT "/device" endpoint + * is used for updates and the POST "/" + * endpoints are used for creation. + * @param {State} data the device to update. + * @returns {Promise} promise that resolves to the saved device and rejects + * with user-fiendly errors as a RemoteError + */ + saveState: (data) => { + return (dispatch) => { + let url = + "/" + data.kind + "/" + data.id + "/state?sceneId=" + data.sceneId; + + return Endpoint["post"](url, {}, data) + .then((res) => { + dispatch(actions.stateSave(res.data)); + return res.data; + }) + .catch((err) => { + console.warn("Update device: ", data, "error: ", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** * Connetcs a series of output devices to an input device. * Output devices for Switch input can be: Normal Light, Dimmable Light, Smart Plug. @@ -452,6 +583,23 @@ export const RemoteService = { }; }, + /** + * Deletes a scene + * @param {Number} sceneId the id of the scene to delete + * @returns {Promise} promise that resolves to void and rejects + * with user-fiendly errors as a RemoteError + */ + deleteScene: (sceneId) => { + return (dispatch) => { + return Endpoint.delete(`/scene/${sceneId}`) + .then((_) => dispatch(actions.sceneDelete(sceneId))) + .catch((err) => { + console.warn("Scene deletion error", err); + throw new RemoteError(["Network error"]); + }); + }; + }, + /** * Deletes a device * @param {Device} device the device to delete @@ -487,7 +635,7 @@ export class Forms { * with validation errors as a String array */ static submitRegistration(data) { - return Endpoint.post( + return Endpoint.postNA( "/register", {}, { @@ -510,7 +658,7 @@ export class Forms { * with validation errors as a String array */ static submitInitResetPassword(email) { - return Endpoint.post( + return Endpoint.postNA( "/register/init-reset-password", {}, { @@ -535,7 +683,7 @@ export class Forms { * with validation errors as a String array */ static submitResetPassword(confirmationToken, password) { - return Endpoint.post( + return Endpoint.putNA( "/register/reset-password", {}, { diff --git a/smart-hut/src/store.js b/smart-hut/src/store.js index 7b04a89..930f387 100644 --- a/smart-hut/src/store.js +++ b/smart-hut/src/store.js @@ -38,6 +38,35 @@ function reducer(previousState, action) { } }; + const createOrUpdateScene = (scene) => { + if (!newState.scenes[scene.id]) { + newState = update(newState, { + scenes: { [scene.id]: { $set: { ...scene, sceneStates: new Set() } } }, + }); + } else { + newState = update(newState, { + scenes: { + [scene.id]: { + name: { $set: scene.name }, + }, + }, + }); + } + + if (newState.pendingJoins.scenes[scene.id]) { + newState = update(newState, { + pendingJoins: { scenes: { $unset: [scene.id] } }, + scenes: { + [scene.id]: { + sceneStates: { + $add: [...newState.pendingJoins.scenes[scene.id]], + }, + }, + }, + }); + } + }; + const updateDeviceProps = (device) => { // In some updates the information regarding a device is incomplete // due to a fault in the type system and JPA repository management @@ -50,6 +79,11 @@ function reducer(previousState, action) { } }; + const updateSceneStateProps = (state) => { + change.sceneStates[state.deviceId] = {}; + change.sceneStates[state.deviceId] = { $set: state.deviceId }; + }; + switch (action.type) { case "LOGIN_UPDATE": newState = update(previousState, { login: { $set: action.login } }); @@ -62,6 +96,73 @@ function reducer(previousState, action) { for (const room of action.rooms) { createOrUpdateRoom(room); } + break; + case "SCENES_UPDATE": + newState = previousState; + for (const scene of action.scenes) { + createOrUpdateScene(scene); + } + break; + case "STATE_UPDATE": + newState = previousState; + change = null; + + // if room is given, delete all devices in that room + // and remove any join between that room and deleted + // devices + change = { + scenes: { [action.sceneId]: { sceneStates: { $set: new Set() } } }, + sceneStates: { $unset: [] }, + }; + + const scene = newState.scenes[action.sceneId]; + for (const stateId of scene.sceneStates) { + change.sceneStates.$unset.push(stateId); + } + + newState = update(previousState, change); + + change = { + sceneStates: {}, + scenes: {}, + pendingJoins: { scenes: {} }, + }; + + for (const sceneState of action.sceneStates) { + if (!newState.sceneStates[sceneState.deviceId]) { + change.sceneStates[sceneState.deviceId] = { + $set: sceneState.deviceId, + }; + } else { + updateSceneStateProps(sceneState); + } + + if (sceneState.sceneId in newState.scenes) { + change.scenes[sceneState.sceneId] = + change.scenes[sceneState.sceneId] || {}; + change.scenes[sceneState.sceneId].sceneStates = + change.scenes[sceneState.sceneId].sceneStates || {}; + const sceneStates = change.scenes[sceneState.sceneId].sceneStates; + sceneStates.$add = sceneStates.$add || []; + sceneStates.$add.push(sceneState.deviceId); + } else { + // room does not exist yet, so add to the list of pending + // joins + + if (!change.pendingJoins.scenes[sceneState.sceneId]) { + change.pendingJoins.scenes[sceneState.sceneId] = { + $set: new Set([sceneState.deviceId]), + }; + } else { + change.pendingJoins.scenes[sceneState.sceneId].$set.add( + sceneState.deviceId + ); + } + } + } + + newState = update(newState, change); + break; case "DEVICES_UPDATE": change = null; @@ -89,12 +190,16 @@ function reducer(previousState, action) { }; for (const device of action.devices) { - const roomId = previousState.devices[device.id].roomId; - change.rooms[roomId] = change.rooms[roomId] || { - devices: { $remove: [] }, - }; - change.rooms[roomId].devices.$remove.push(device.id); + if (!previousState.devices[device.id]) continue; change.devices.$unset.push(device.id); + const roomId = previousState.devices[device.id].roomId; + + if (roomId in previousState.rooms) { + change.rooms[roomId] = change.rooms[roomId] || { + devices: { $remove: [] }, + }; + change.rooms[roomId].devices.$remove.push(device.id); + } } } else { // otherwise, just delete all devices and all joins @@ -152,6 +257,10 @@ function reducer(previousState, action) { newState = previousState; createOrUpdateRoom(action.room); break; + case "SCENE_SAVE": + newState = previousState; + createOrUpdateScene(action.scene); + break; case "DEVICE_SAVE": change = { devices: { [action.device.id]: { $set: action.device } }, @@ -176,6 +285,31 @@ function reducer(previousState, action) { } newState = update(previousState, change); break; + case "STATE_SAVE": + console.log("Store", action.sceneState); + change = { + sceneStates: { [action.sceneState.id]: { $set: action.sceneState } }, + }; + + if (previousState.scenes[action.sceneState.sceneId]) { + change.scenes = { + [action.sceneState.sceneId]: { + sceneStates: { + $add: [action.sceneState.id], + }, + }, + }; + } else { + change.pendingJoins = { + scenes: { + [action.sceneState.sceneId]: { + $add: [action.sceneState.id], + }, + }, + }; + } + newState = update(previousState, change); + break; case "ROOM_DELETE": if (!(action.roomId in previousState.rooms)) { console.warn(`Room to delete ${action.roomId} does not exist`); @@ -197,6 +331,30 @@ function reducer(previousState, action) { change.active = { activeRoom: { $set: -1 } }; } + newState = update(previousState, change); + break; + case "SCENE_DELETE": + console.log("SCENE", action.sceneId); + if (!(action.sceneId in previousState.scenes)) { + console.warn(`Scene to delete ${action.sceneId} does not exist`); + break; + } + + // This update does not ensure the consistent update of switchId/dimmerId properties + // on output devices connected to an input device in this room. Please manually request + // all devices again if consistent update is desired + change = { states: { $unset: [] } }; + + for (const id of previousState.scenes[action.sceneId].sceneStates) { + change.sceneStates.$unset.push(id); + } + + change.scenes = { $unset: [action.sceneId] }; + + if (previousState.active.activeScene === action.sceneId) { + change.active = { activeScene: { $set: -1 } }; + } + newState = update(previousState, change); break; case "DEVICE_DELETE": @@ -231,6 +389,33 @@ function reducer(previousState, action) { }, }); break; + case "SET_ACTIVE_TAB": + newState = update(previousState, { + active: { + activeTab: { + $set: action.activeTab, + }, + }, + }); + break; + case "SET_ACTIVE_SCENE": + newState = update(previousState, { + active: { + activeScene: { + $set: action.activeScene, + }, + }, + }); + break; + case "SET_ACTIVE_AUTOMATION": + newState = update(previousState, { + active: { + activeAutomation: { + $set: action.activeAutomation, + }, + }, + }); + break; case "REDUX_WEBSOCKET::MESSAGE": const devices = JSON.parse(action.payload.message); console.log(devices); @@ -249,12 +434,16 @@ function reducer(previousState, action) { } const initState = { - errors: {}, pendingJoins: { rooms: {}, + scenes: {}, + automations: {}, }, active: { activeRoom: -1, + activeTab: "Devices", + activeScene: -1, + activeAutomation: -1, }, login: { loggedIn: false, @@ -263,8 +452,14 @@ const initState = { userInfo: null, /** @type {[integer]Room} */ rooms: {}, + /** @type {[integer]Scene} */ + scenes: {}, + /** @type {[integer]Automation} */ + automations: {}, /** @type {[integer]Device} */ devices: {}, + /** @type {[integer]SceneState} */ + sceneStates: {}, }; function createSmartHutStore() { diff --git a/smart-hut/src/storeActions.js b/smart-hut/src/storeActions.js index 3a20fee..3ca8bcf 100644 --- a/smart-hut/src/storeActions.js +++ b/smart-hut/src/storeActions.js @@ -17,10 +17,23 @@ const actions = { type: "ROOM_SAVE", room, }), + sceneSave: (scene) => ({ + type: "SCENE_SAVE", + scene, + }), deviceSave: (device) => ({ type: "DEVICE_SAVE", device, }), + stateSave: (sceneState) => ({ + type: "STATE_SAVE", + sceneState, + }), + statesUpdate: (sceneId, sceneStates) => ({ + type: "STATE_UPDATE", + sceneId, + sceneStates, + }), devicesUpdate: (roomId, devices, partial = false) => ({ type: "DEVICES_UPDATE", roomId, @@ -40,6 +53,14 @@ const actions = { type: "ROOM_DELETE", roomId, }), + sceneDelete: (sceneId) => ({ + type: "SCENE_DELETE", + sceneId, + }), + scenesUpdate: (scenes) => ({ + type: "SCENES_UPDATE", + scenes, + }), deviceDelete: (deviceId) => ({ type: "DEVICE_DELETE", deviceId, @@ -52,6 +73,18 @@ export const appActions = { type: "SET_ACTIVE_ROOM", activeRoom, }), + setActiveTab: (activeTab) => ({ + type: "SET_ACTIVE_TAB", + activeTab, + }), + setActiveScene: (activeScene = -1) => ({ + type: "SET_ACTIVE_SCENE", + activeScene, + }), + setActiveAutomations: (activeAutomation = -1) => ({ + type: "SET_ACTIVE_AUTOMATION", + activeAutomation, + }), }; export default actions; diff --git a/smart-hut/src/views/AutomationsNavbar.js b/smart-hut/src/views/AutomationsNavbar.js new file mode 100644 index 0000000..3378a90 --- /dev/null +++ b/smart-hut/src/views/AutomationsNavbar.js @@ -0,0 +1,165 @@ +import React, { Component } from "react"; +import { + Menu, + Button, + Grid, + Icon, + Responsive, + Dropdown, +} from "semantic-ui-react"; +import { editButtonStyle } from "../components/dashboard/devices/styleComponents"; +import AutomationModal from "../components/AutomationModal"; +import { RemoteService } from "../remote"; +import { connect } from "react-redux"; +import { appActions } from "../storeActions"; + +class AutomationsNavbar extends Component { + constructor(props) { + super(props); + this.state = { + editMode: false, + }; + + this.toggleEditMode = this.toggleEditMode.bind(this); + this.openCurrentModalMobile = this.openCurrentModalMobile.bind(this); + } + + get activeItemAutomation() { + return this.props.activeAutomation; + } + + set activeItemAutomation(item) { + this.props.setActiveAutomation(item); + } + + get activeItemAutomationsName() { + if (this.props.activeAutomation === -1) return "Home"; + return this.props.automations[this.props.activeAutomation].name; + } + + openCurrentModalMobile() { + console.log(this.activeItemAutomation, this.props.automationsModalRefs); + const currentModal = this.props.automationsModalRefs[ + this.activeItemAutomation + ].current; + currentModal.openModal(); + } + + toggleEditMode(e) { + this.setState((prevState) => ({ editMode: !prevState.editMode })); + } + + render() { + return ( +
+ + + + + + + + + + + + + + AUTOMATIONS + + + + { + //INSERT LIST OF AUTOMATIONS HERE + } + + + + + + + + + + + + + + + + + + + + + + + Automations + + + + + { + //INSERT LIST OF AUTOMATIONS HERE + } + + + + + + + + + {this.activeItemAutomation !== -1 ? ( + + + + ) : null} + + + +
+ ); + } +} + +const setActiveAutomation = (activeAutomation) => { + return (dispatch) => + dispatch(appActions.setActiveAutomation(activeAutomation)); +}; + +const mapStateToProps = (state, _) => ({ + automations: state.automations, + activeAutomation: state.active.activeAutomation, + automationModalRefs: Object.keys(state.automations).reduce( + (acc, key) => ({ ...acc, [key]: React.createRef() }), + {} + ), +}); +const AutomationsNavbarContainer = connect(mapStateToProps, { + ...RemoteService, + setActiveAutomation, +})(AutomationsNavbar); +export default AutomationsNavbarContainer; diff --git a/smart-hut/src/views/ConfirmForgotPassword.js b/smart-hut/src/views/ConfirmForgotPassword.js index 99b194b..1f4d0ec 100644 --- a/smart-hut/src/views/ConfirmForgotPassword.js +++ b/smart-hut/src/views/ConfirmForgotPassword.js @@ -1,79 +1,40 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; +export default class ConfirmForgotPasswrod extends Component { render() { - if (this.state.visible) { - return ( - - ); - } - return ( -

-
- The message will return in 2s -
-
-

+ + + + +
+ Link has been sent! +
+ +

+ An E-mail has been sent to your address, please follow the + instructions to create a new password. If you don't find the + E-mail please check also the spam folder. +

+
+
+
+
); } } - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default class ConfirmForgotPasswrod extends React.Component { - render() { - return ; - } -} diff --git a/smart-hut/src/views/ConfirmRegistration.js b/smart-hut/src/views/ConfirmRegistration.js index 13af1b5..05adedd 100644 --- a/smart-hut/src/views/ConfirmRegistration.js +++ b/smart-hut/src/views/ConfirmRegistration.js @@ -1,79 +1,40 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; +export default class ConfirmRegistration extends Component { render() { - if (this.state.visible) { - return ( - - ); - } - return ( -

-
- The message will return in 2s -
-
-

+ + + + +
+ Congratulation! +
+ +

+ An E-mail has been sent to your address, confirm your + registration by following the enclosed link. If you don't find + the E-mail please check also the spam folder. +

+
+
+
+
); } } - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default class ConfirmRegistration extends React.Component { - render() { - return ; - } -} diff --git a/smart-hut/src/views/ConfirmResetPassword.js b/smart-hut/src/views/ConfirmResetPassword.js index 0d6123d..75c0bdc 100644 --- a/smart-hut/src/views/ConfirmResetPassword.js +++ b/smart-hut/src/views/ConfirmResetPassword.js @@ -1,79 +1,36 @@ import React, { Component } from "react"; -import HomeNavbar from "./../components/HomeNavbar"; -import { Image, Divider, Message, Grid } from "semantic-ui-react"; - -class Paragraph extends Component { - state = { visible: true }; - - handleDismiss = () => { - this.setState({ visible: false }); - - setTimeout(() => { - this.setState({ visible: true }); - }, 2000); - }; - - render() { - if (this.state.visible) { - return ( - - - Congratulations! - - Your password has been successfully reset - - ); - } - - return ( -

-
- The message will return in 2s -
-
-

- ); - } -} - -const MessageReg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); +import { + Image, + Grid, + Button, + Icon, + Header, + Container, +} from "semantic-ui-react"; export default class ConfirmResetPassword extends Component { render() { - return ; + return ( + + + + +
+ Congratulation! +
+ +

Your password has been successfully reset.

+
+
+
+
+ ); } } diff --git a/smart-hut/src/views/Dashboard.js b/smart-hut/src/views/Dashboard.js index 06c10ef..651a1e5 100644 --- a/smart-hut/src/views/Dashboard.js +++ b/smart-hut/src/views/Dashboard.js @@ -1,13 +1,82 @@ import React, { Component } from "react"; import DevicePanel from "../components/dashboard/DevicePanel"; +import ScenesPanel from "../components/dashboard/ScenesPanel"; +import AutomationsPanel from "../components/dashboard/AutomationsPanel"; import Navbar from "./Navbar"; +import ScenesNavbar from "./ScenesNavbar"; +import AutomationsNavbar from "./AutomationsNavbar"; import MyHeader from "../components/HeaderController"; -import { Grid, Responsive } from "semantic-ui-react"; +import { Grid, Responsive, Button } from "semantic-ui-react"; +import { + panelStyle, + mobilePanelStyle, +} from "../components/dashboard/devices/styleComponents"; + +import { RemoteService } from "../remote"; +import { connect } from "react-redux"; +import { appActions } from "../storeActions"; + +class Dashboard extends Component { + constructor(props) { + super(props); + this.state = this.initialState; + this.setInitialState(); + + this.selectTab = this.selectTab.bind(this); + } + + get initialState() { + return { + activeTab: this.activeTab, + }; + } + + setInitialState() { + this.setState(this.initialState); + } + + get activeTab() { + return this.props.activeTab; + } + + set activeTab(tab) { + this.props.setActiveTab(tab); + } + + selectTab(e, { name }) { + this.setState({ activeTab: name }); + this.activeTab = name; + } + + renderTab(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; + } + } + + renderNavbar(tab) { + switch (tab) { + case "Devices": + return ; + case "Scenes": + return ; + case "Automations": + return ; + default: + return

ERROR

; + } + } -export default class Dashboard extends Component { render() { return ( -
+
@@ -15,13 +84,42 @@ export default class Dashboard extends Component { + + + + + + + + + SCENES + + + {Object.values(this.props.scenes).map((e, i) => { + return ( + + + + {e.name} + + {this.state.editMode ? ( + + ) : null} + + + + + ); + })} + + + + + + + + + + + + + + + + + + + + + Scene + + + + + {Object.values(this.props.scenes).map((e, i) => { + return ( + + + + {e.name} + + + + + ); + })} + + + + + + + + + {this.activeItemScene !== -1 ? ( + + + + ) : null} + + + +
+ ); + } +} + +const setActiveScene = (activeScene) => { + return (dispatch) => dispatch(appActions.setActiveScene(activeScene)); +}; + +const mapStateToProps = (state, _) => ({ + scenes: state.scenes, + sceneModalRefs: Object.keys(state.scenes).reduce( + (acc, key) => ({ ...acc, [key]: React.createRef() }), + {} + ), + get isActiveDefaultScene() { + return state.active.activeScene === -1; + }, + activeScene: state.active.activeScene, +}); +const ScenesNavbarContainer = connect(mapStateToProps, { + ...RemoteService, + setActiveScene, +})(ScenesNavbar); +export default ScenesNavbarContainer;