Merge branch 'mod-room' of lab.si.usi.ch:sa4-2020/the-sanmarinoes/frontend into mod-room
This commit is contained in:
commit
6bf3bd07fa
23 changed files with 845 additions and 273 deletions
|
@ -3,6 +3,7 @@ image: node:latest
|
|||
stages:
|
||||
- build
|
||||
- test
|
||||
- deploy
|
||||
|
||||
cache:
|
||||
paths:
|
||||
|
@ -22,3 +23,22 @@ testing_testing:
|
|||
script:
|
||||
- cd smart-hut
|
||||
- yarn test
|
||||
|
||||
smartHut_deploy:
|
||||
stage: deploy
|
||||
# tags:
|
||||
# - dind
|
||||
image: docker:latest
|
||||
services:
|
||||
- docker:dind
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay
|
||||
before_script:
|
||||
- docker version
|
||||
- docker info
|
||||
- docker login -u smarthutsm -p $CI_DOCKER_PASS #GiovanniRoberto
|
||||
script:
|
||||
- "docker build -t smarthutsm/smarthut:${CI_COMMIT_BRANCH} ."
|
||||
- "docker push smarthutsm/smarthut:${CI_COMMIT_BRANCH}"
|
||||
after_script:
|
||||
- docker logout
|
||||
|
|
39
Dockerfile
Normal file
39
Dockerfile
Normal file
|
@ -0,0 +1,39 @@
|
|||
#FROM mhart/alpine-node:11 AS pleaseGodWork
|
||||
#WORKDIR /app
|
||||
#COPY . /app
|
||||
#RUN ls
|
||||
#RUN yarn run build
|
||||
#
|
||||
#RUN yarn global add serve
|
||||
#
|
||||
#
|
||||
#CMD ["serve", "-p", "8080", "-s", "."]
|
||||
|
||||
# base image
|
||||
FROM node:9.6.1
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
|
||||
|
||||
COPY smart-hut/package.json /usr/src/app/package.json
|
||||
RUN npm install --silent
|
||||
RUN npm install react-scripts@1.1.1 -g --silent
|
||||
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
FROM node:9.6.1 as builder
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
COPY smart-hut/package.json /usr/src/app/package.json
|
||||
RUN npm install --silent
|
||||
RUN npm install react-scripts@1.1.1 -g --silent
|
||||
COPY smart-hut/. /usr/src/app
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.13.9-alpine
|
||||
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
|
||||
EXPOSE 8080
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
1
smart-hut/public/img/smart-plug-off.svg
Normal file
1
smart-hut/public/img/smart-plug-off.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" enable-background="new 0 0 511.929 511.929" height="512" viewBox="0 0 511.929 511.929" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m176.652 174.329v57.6h32v-57.6c0-8.84-7.16-16-16-16-8.83 0-16 7.16-16 16z"/><path d="m303.592 174.329v57.6h32v-57.6c0-8.84-7.17-16-16-16-8.84 0-16 7.16-16 16z"/><path d="m344.052 263.929h-175.86c-8.84 0-16 7.16-16 16 0 8.83 7.16 16 16 16h4.2v41.6c0 21.17 17.22 38.4 38.4 38.4h90.66c21.18 0 38.4-17.23 38.4-38.4v-41.6h4.2c8.84 0 16-7.17 16-16 0-8.84-7.16-16-16-16z"/><path d="m224.122 407.929v8.53c0 11.843 6.438 22.175 16 27.708v51.762c0 8.84 7.16 16 16 16s16-7.16 16-16v-51.762c9.562-5.534 16-15.866 16-27.708v-8.53c-24.422 0-39.67 0-64 0z"/><path d="m193.731 89.704c-6.249 6.248-6.249 16.379 0 22.627 6.248 6.248 16.379 6.249 22.627 0 21.886-21.885 57.309-21.887 79.196 0 3.124 3.124 7.219 4.686 11.313 4.686 14.126 0 21.422-17.206 11.313-27.314-34.391-34.39-90.057-34.394-124.449.001z"/><path d="m363.436 44.448c-59.263-59.264-155.695-59.266-214.96 0-6.249 6.249-6.249 16.379 0 22.627 6.248 6.249 16.379 6.249 22.627 0 46.897-46.896 122.804-46.901 169.706 0 3.124 3.125 7.219 4.687 11.313 4.687 14.126 0 21.422-17.206 11.314-27.314z"/><circle cx="255.955" cy="151.929" r="24"/></g></svg>
|
After Width: | Height: | Size: 1.2 KiB |
1
smart-hut/public/img/smart-plug.svg
Normal file
1
smart-hut/public/img/smart-plug.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" enable-background="new 0 0 511.929 511.929" height="512" viewBox="0 0 511.929 511.929" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m343.964 263.929v56c0 30.93-25.07 56-56 56h-64c-30.924 0-56-25.071-56-56v-56z" fill="#a2d0ff"/><path d="m343.964 263.929v32h-82.04c-38.53 0-69.96 31.216-69.96 69.96-14.51-10.12-24-26.93-24-45.96v-56z" fill="#8bc4ff"/><path d="m255.965 439.929c-17.624 0-32-14.278-32-32v-32h64v32c-.001 17.673-14.327 32-32 32z" fill="#8bc4ff"/><g fill="#2e58ff"><path d="m367.964 247.929c-13.903 0-26.139 0-40 0v-64c0-8.836-7.163-16-16-16s-16 7.164-16 16v64h-80v-64c0-8.836-7.163-16-16-16s-16 7.164-16 16v64c-13.873 0-26.083 0-40 0-8.837 0-16 7.164-16 16s7.163 16 16 16h8v48c0 32.579 24.475 59.531 56 63.482v16.518c0 20.858 13.377 38.643 32 45.248v42.752c0 8.836 7.163 16 16 16s16-7.164 16-16v-42.752c18.623-6.605 32-24.389 32-45.248v-16.518c31.525-3.951 56-30.904 56-63.482v-48h8c8.837 0 16-7.164 16-16s-7.163-16-16-16zm-96 160c0 8.822-7.178 16-16 16s-16-7.178-16-16v-16h32zm56-80c0 17.645-14.355 32-32 32-16.043 0-63.799 0-80 0-17.645 0-32-14.355-32-32v-48h144z"/><circle cx="255.795" cy="151.929" r="24"/><path d="m306.71 117.017c14.126 0 21.422-17.206 11.313-27.314-34.392-34.391-90.058-34.394-124.45 0-6.249 6.248-6.249 16.379 0 22.627 6.248 6.248 16.379 6.249 22.627 0 21.886-21.885 57.309-21.887 79.196 0 3.124 3.125 7.219 4.687 11.314 4.687z"/><path d="m170.945 67.076c46.897-46.896 122.804-46.901 169.706 0 6.247 6.248 16.379 6.249 22.627 0 6.249-6.248 6.249-16.379 0-22.627-59.263-59.264-155.695-59.266-214.96 0-6.249 6.249-6.249 16.379 0 22.627 6.248 6.249 16.379 6.249 22.627 0z"/></g></svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -59,6 +59,28 @@ export var call = {
|
|||
return err;
|
||||
});
|
||||
},
|
||||
getAllDevices: function(token) {
|
||||
if (!token){
|
||||
token = tkn;
|
||||
}
|
||||
return axios.get(config + 'device', { headers: { Authorization : "Bearer " + token } })
|
||||
.then(res => {
|
||||
return res;
|
||||
}).catch(err => {
|
||||
return err;
|
||||
});
|
||||
},
|
||||
getAllDevicesByRoom: function(id, token) {
|
||||
if (!token){
|
||||
token = tkn;
|
||||
}
|
||||
return axios.get(config + 'room/' + id + '/device' , { headers: { Authorization : "Bearer " + token } })
|
||||
.then(res => {
|
||||
return res;
|
||||
}).catch(err => {
|
||||
return err;
|
||||
});
|
||||
},
|
||||
createRoom: function(data, headers) {
|
||||
return axios.post(config + 'room', data, { headers: { Authorization : "Bearer " + tkn } })
|
||||
.then(res => {
|
||||
|
|
|
@ -2,59 +2,107 @@ import React, {Component} from 'react';
|
|||
import {
|
||||
Grid,
|
||||
} from "semantic-ui-react";
|
||||
import Device from "./devices/Device";
|
||||
import NewDevice from "./devices/NewDevice";
|
||||
import {LightDevice, TemperatureSensor} from "./devices/TypesOfDevices";
|
||||
import {editButtonStyle, panelStyle} from "./devices/styleComponents";
|
||||
import {checkMaxLength, DEVICE_NAME_MAX_LENGTH} from "./devices/constants";
|
||||
import DeviceType from './devices/DeviceTypeController';
|
||||
import NewDevice from "./devices/NewDevice";
|
||||
|
||||
class Panel extends Component {
|
||||
|
||||
/* const devices = [
|
||||
{
|
||||
"id" : 1,
|
||||
"name": "Bedroom Light",
|
||||
"type" : "light",
|
||||
"hasIntensity" : true,
|
||||
"intensityLevel" : 0.20,
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"id" : 2,
|
||||
"name": "Bathroom Light",
|
||||
"type" : "light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"id" : 3,
|
||||
"name": "Desktop Light",
|
||||
"type" : "light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"id" : 4,
|
||||
"name": "Entrance Light",
|
||||
"type" : "light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"id" : 5,
|
||||
"name": "Smart Plug",
|
||||
"type" : "smartplug",
|
||||
...SmartPlugDevice
|
||||
},
|
||||
{
|
||||
"id" : 6,
|
||||
"name": "Bedroom Thermometer",
|
||||
"type" : "temperature-sensor",
|
||||
},
|
||||
];*/
|
||||
|
||||
export default class DevicePanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
editMode : false
|
||||
editMode : false,
|
||||
devices : this.props.devices,
|
||||
};
|
||||
}
|
||||
|
||||
editModeController = (e) => {
|
||||
this.setState((prevState) => ({ editMode: !prevState.editMode }));
|
||||
editModeController = (e) => this.setState((prevState) => ({ editMode: !prevState.editMode }));
|
||||
|
||||
changeDeviceData = (deviceId, newSettings) => {
|
||||
console.log(newSettings.name, " <-- new name --> ", deviceId );
|
||||
this.state.devices.map(device => {
|
||||
if(device.id === deviceId){
|
||||
for(let prop in newSettings){
|
||||
if(device.hasOwnProperty(prop)){
|
||||
if(prop==="name"){
|
||||
if(checkMaxLength(newSettings[prop])){
|
||||
device[prop] = newSettings[prop];
|
||||
}
|
||||
else{
|
||||
alert("Name must be less than " + DEVICE_NAME_MAX_LENGTH + " characters.");
|
||||
}
|
||||
}else{
|
||||
device[prop] = newSettings[prop];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
const devices = [
|
||||
{
|
||||
"name": "Bedroom Light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"name": "Bathroom Light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"name": "Desktop Light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"name": "Entrance Light",
|
||||
...LightDevice
|
||||
},
|
||||
{
|
||||
"name": "Bedroom",
|
||||
...TemperatureSensor
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={panelStyle}>
|
||||
<button style={editButtonStyle} onClick={this.editModeController}>Edit</button>
|
||||
<Grid doubling columns={5} divided="vertically">
|
||||
{devices.map((e, i) => {
|
||||
<Grid doubling divided="vertically">
|
||||
{
|
||||
this.props.devices ?
|
||||
this.props.devices.map((e, i) => {
|
||||
return (
|
||||
<Grid.Column key={i.toString()}>
|
||||
<Device device={e} edit={this.state.editMode}/>
|
||||
<Grid.Column>
|
||||
<DeviceType type={e.type} onChangeData={this.changeDeviceData} device={e} edit={this.state.editMode}/>
|
||||
</Grid.Column>
|
||||
)
|
||||
})}
|
||||
})
|
||||
:
|
||||
null
|
||||
}
|
||||
<Grid.Column>
|
||||
<NewDevice/>
|
||||
</Grid.Column>
|
||||
|
@ -65,21 +113,3 @@ class Panel extends Component {
|
|||
|
||||
}
|
||||
|
||||
|
||||
export default class DevicePanel extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shownRoom: "All"
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Panel/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,44 +1,8 @@
|
|||
import React, {Component} from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {Image} from "semantic-ui-react";
|
||||
import Sensor from './Sensor';
|
||||
import {editModeIconStyle, editModeStyle} from "./styleComponents";
|
||||
import {iconStyle, nameStyle, StyledDiv} from "./styleComponents";
|
||||
import Settings from './DeviceSettings';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
background-color : white;
|
||||
padding : 3rem;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
border-radius : 100%;
|
||||
border : none;
|
||||
position : relative;
|
||||
box-shadow: 3px 2px 10px 5px #ccc;
|
||||
transition : all .3s ease-out;
|
||||
:hover{
|
||||
background-color : #f2f2f2;
|
||||
}
|
||||
:active{
|
||||
transform : translate(0.3px, 0.8px);
|
||||
box-shadow: 0.5px 0.5px 7px 3.5px #ccc;
|
||||
}
|
||||
`;
|
||||
|
||||
const iconStyle = {
|
||||
width: "4rem",
|
||||
height: "auto",
|
||||
position: "absolute",
|
||||
top: "20%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
filter: "drop-shadow( 1px 1px 0.5px rgba(0, 0, 0, .25))"
|
||||
};
|
||||
|
||||
const nameStyle = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
}
|
||||
|
||||
export default class Device extends Component {
|
||||
constructor(props) {
|
||||
|
@ -50,6 +14,7 @@ export default class Device extends Component {
|
|||
}
|
||||
|
||||
onClickDevice = () => {
|
||||
if (!this.props.edit) {
|
||||
if (this.props.device.type === "light") {
|
||||
if (this.state.turnOnOff === "on") {
|
||||
this.setState({
|
||||
|
@ -63,20 +28,16 @@ export default class Device extends Component {
|
|||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.device.type === "temperature_sensor") {
|
||||
return (
|
||||
<StyledDiv>
|
||||
{this.props.edit ? (<span style={editModeStyle}><img src="/img/settings.svg" style={editModeIconStyle}/></span>) : ("")}
|
||||
<Sensor device={this.props.device}/>
|
||||
</StyledDiv>
|
||||
)
|
||||
}
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<StyledDiv onClick={this.onClickDevice} style={{textAlign: "center"}}>
|
||||
{this.props.edit ? (<span style={editModeStyle}><img src="/img/settings.svg" style={editModeIconStyle}/></span>) : ("")}
|
||||
<StyledDiv onClick={this.props.edit ? () => {
|
||||
} : this.onClickDevice} style={{textAlign: "center"}}>
|
||||
<Settings
|
||||
deviceId={this.props.device.id}
|
||||
edit={this.props.edit}
|
||||
onChangeData={(id, newSettings) => this.props.onChangeData(id, newSettings)}/>
|
||||
<Image src={this.state.icon} style={iconStyle}/>
|
||||
<h5 style={nameStyle}>{this.props.device.name}</h5>
|
||||
</StyledDiv>
|
||||
|
|
73
smart-hut/src/components/dashboard/devices/DeviceSettings.js
Normal file
73
smart-hut/src/components/dashboard/devices/DeviceSettings.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React, {Component} from "react";
|
||||
import {Button, Form} from "semantic-ui-react";
|
||||
import {editModeIconStyle, editModeStyle, formStyle} from "./styleComponents";
|
||||
|
||||
class SettingsForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {}
|
||||
};
|
||||
|
||||
onChangeHandler = (event) => {
|
||||
let nam = event.target.name;
|
||||
let val = event.target.value;
|
||||
this.setState({[nam]: val});
|
||||
};
|
||||
|
||||
saveChanges = () => {
|
||||
let newName = this.state["new-name"];
|
||||
this.props.onChangeData(this.props.id, {"name": newName});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Form style={formStyle}>
|
||||
<Form.Field>
|
||||
<label style={{color: "white"}}>New Device Name</label>
|
||||
<input name="new-name" placeholder='New name' onChange={this.onChangeHandler}/>
|
||||
</Form.Field>
|
||||
<Button type='submit' onClick={this.saveChanges}>Save</Button>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class Settings extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
displayForm: false,
|
||||
}
|
||||
};
|
||||
|
||||
displayForm = () => {
|
||||
this.setState((prevState) => ({displayForm: !prevState.displayForm}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const view = (
|
||||
<div>
|
||||
{this.state.displayForm ? (
|
||||
<SettingsForm id={this.props.deviceId} onChangeData={this.props.onChangeData}/>) : ("")}
|
||||
<div onClick={this.displayForm}>
|
||||
<span style={editModeStyle}>
|
||||
{!this.state.displayForm ? (
|
||||
<img
|
||||
src="/img/settings.svg"
|
||||
alt=""
|
||||
style={editModeIconStyle}/>)
|
||||
:
|
||||
<p style={{color: "white"}}>×</p>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.edit ? view : ("")}
|
||||
</React.Fragment>
|
||||
)
|
||||
};
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import React, { Component } from 'react';
|
||||
import Light from "./Light";
|
||||
import SmartPlug from "./SmartPlug";
|
||||
import Sensor from "./Sensor";
|
||||
import DefaultDimmer from "./Dimmer";
|
||||
import Switcher from "./Switcher";
|
||||
|
||||
|
||||
|
||||
const DeviceType = (props) => {
|
||||
switch(props.type) {
|
||||
case "light":
|
||||
return <Light onChangeData={props.changeDeviceData} device={props.device} edit={props.edit} />
|
||||
case "sensor":
|
||||
return <Sensor onChangeData={props.changeDeviceData} device={props.device} edit={props.edit} />
|
||||
case "dimmer":
|
||||
return <DefaultDimmer onChangeData={props.changeDeviceData} device={props.device} edit={props.edit} />
|
||||
case "smartplug":
|
||||
return <SmartPlug onChangeData={props.changeDeviceData} device={props.device} edit={props.edit} />
|
||||
case "switch":
|
||||
return <Switcher onChangeData={props.changeDeviceData} device={props.device} edit={props.edit} />
|
||||
}
|
||||
}
|
||||
|
||||
export default DeviceType;
|
52
smart-hut/src/components/dashboard/devices/Dimmer.js
Normal file
52
smart-hut/src/components/dashboard/devices/Dimmer.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
Users can add dimmers, a particular kind of switch that can also modify the intensity level of a given light.
|
||||
There are two types of dimmers:
|
||||
• A dimmer with state stores a given intensity level and sets the light to that level. <-- StatefulDimmer
|
||||
• A dimmer without state can just increase or decrease the intensity of a light. <-- DefualtDimmer
|
||||
|
||||
The user can change the state of a dimmer through an intuitive UI in SmartHut .
|
||||
**/
|
||||
|
||||
import React, {Component} from 'react';
|
||||
|
||||
export class StatefulDimmer extends Component{
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
intensityLevel : 0,
|
||||
pointingLDevices:[]
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
This is a Dimmer
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DefaultDimmer extends Component{
|
||||
// As far as I am concern, even though this dimmer doesn't have state, internally it's needed
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
pointingDevices :[]
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
This is a Dimmer
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
92
smart-hut/src/components/dashboard/devices/Light.js
Normal file
92
smart-hut/src/components/dashboard/devices/Light.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Users can add lights in their rooms.
|
||||
* Lights are devices like bulbs, LED strip lights, lamps.
|
||||
* Lights may support an intensity level (from 0% to 100%).
|
||||
* Lights have an internal state that can be changed and it must
|
||||
* be shown accordingly in the SmartHut views (house view and room views).
|
||||
*/
|
||||
|
||||
import React, {Component} from "react";
|
||||
import {iconStyle, StyledDiv, nameStyle} from "./styleComponents";
|
||||
import Settings from "./DeviceSettings";
|
||||
import {Image} from "semantic-ui-react";
|
||||
import {CircularInput, CircularProgress, CircularThumb, CircularTrack} from "react-circular-input";
|
||||
import {valueStyle, intensityLightStyle, style} from "./LightStyle";
|
||||
|
||||
export default class Light extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
turnedOn: false,
|
||||
hasIntensity : false
|
||||
};
|
||||
this.iconOn = "/img/lightOn.svg";
|
||||
this.iconOff = "/img/lightOff.svg"
|
||||
}
|
||||
|
||||
onClickDevice = () => {
|
||||
this.setState((prevState) => ({turnedOn: !prevState.turnedOn}));
|
||||
};
|
||||
|
||||
setIntensity = (newValue) => {
|
||||
this.setState({intensityLevel : newValue});
|
||||
};
|
||||
|
||||
getIcon = () => {
|
||||
if(this.state.turnedOn){
|
||||
return this.iconOn;
|
||||
}
|
||||
return this.iconOff;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if(this.props.device.hasOwnProperty("hasIntensity") && this.props.device.hasOwnProperty("intensityLevel")) {
|
||||
this.setState({
|
||||
hasIntensity: this.props.device.hasIntensity,
|
||||
intensityLevel: this.props.device.intensityLevel
|
||||
});
|
||||
}
|
||||
// Get the state and update it
|
||||
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const intensityLightView = (
|
||||
<CircularInput
|
||||
value={this.state.intensityLevel}
|
||||
onChange={this.setIntensity}
|
||||
style={style}
|
||||
>
|
||||
<CircularTrack/>
|
||||
<CircularProgress/>
|
||||
<CircularThumb/>
|
||||
|
||||
<text style={valueStyle} x={100} y={100} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{Math.round(this.state.intensityLevel*100)}%
|
||||
</text>
|
||||
<text style={intensityLightStyle} x={100} y={150} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{this.props.device.name}
|
||||
</text>
|
||||
</CircularInput>
|
||||
);
|
||||
|
||||
const normalLightView = (
|
||||
<StyledDiv onClick={this.props.edit ? () => {
|
||||
} : this.onClickDevice} style={{textAlign: "center"}}>
|
||||
<Settings
|
||||
deviceId={this.props.device.id}
|
||||
edit={this.props.edit}
|
||||
onChangeData={(id, newSettings) => this.props.onChangeData(id, newSettings)}/>
|
||||
<Image src={this.getIcon()} style={iconStyle}/>
|
||||
<h5 style={nameStyle}>{this.props.device.name}</h5>
|
||||
</StyledDiv>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.state.hasIntensity ? (intensityLightView) : (normalLightView)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
14
smart-hut/src/components/dashboard/devices/LightStyle.js
Normal file
14
smart-hut/src/components/dashboard/devices/LightStyle.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const style = {width: "10rem", height: "10rem", position: "absolute", top: "0", left: "0"};
|
||||
export const valueStyle = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "2.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
};
|
||||
|
||||
export const intensityLightStyle = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "1.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
};
|
|
@ -1,17 +1,8 @@
|
|||
import React, {Component} from 'react';
|
||||
import styled, {keyframes} from 'styled-components';
|
||||
import {Image} from "semantic-ui-react";
|
||||
|
||||
const rotateAddButton = keyframes`
|
||||
0% {
|
||||
transform : translate(0px, 0px) rotate(0deg);
|
||||
box-shadow: 3px 2px 10px 5px #ccc;
|
||||
}
|
||||
100% {
|
||||
transform : translate(0.3px, 0.8px) rotate(90deg);
|
||||
box-shadow: 0.5px 0.5px 7px 3.5px #ccc;
|
||||
}
|
||||
`;
|
||||
import styled from 'styled-components';
|
||||
import {Button, Dropdown, Form, Image} from "semantic-ui-react";
|
||||
import {addDeviceFormStyle} from "./styleComponents";
|
||||
import {deviceList} from "./TypesOfDevices";
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
background-color : #ff4050;
|
||||
|
@ -26,47 +17,91 @@ const StyledDiv = styled.div`
|
|||
:hover{
|
||||
background-color : #ff2436;
|
||||
}
|
||||
:active{
|
||||
animation-name: ${rotateAddButton};
|
||||
animation-duration: 0.5s;
|
||||
animation-timing-function: ease;
|
||||
animation-delay: 0s;
|
||||
animation-direction: normal;
|
||||
animation-play-state: running;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
`;
|
||||
const iconStyle = {
|
||||
width : "4rem",
|
||||
height : "auto",
|
||||
position : "absolute",
|
||||
top : "20%",
|
||||
left : "50%",
|
||||
transform : "translateX(-50%)"
|
||||
|
||||
class NewDeviceForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
formSelector = (option) => {
|
||||
switch (option) {
|
||||
case "Light":
|
||||
return <LightForm/>;
|
||||
case "Sensor":
|
||||
return "This is a sensor form";
|
||||
default:
|
||||
return "This is a default text"
|
||||
}
|
||||
};
|
||||
|
||||
const nameStyle = {
|
||||
position : "absolute",
|
||||
top : "50%",
|
||||
left : "50%",
|
||||
transform : "translateX(-50%)"
|
||||
render() {
|
||||
let options = [];
|
||||
deviceList.forEach((e, i) => {
|
||||
options.push({key: i, text: e, value: e})
|
||||
});
|
||||
|
||||
return (
|
||||
<Form style={addDeviceFormStyle}>
|
||||
<Form.Field>
|
||||
<label style={{color: "white"}}>Select the type of device</label>
|
||||
<Dropdown clearable options={options} selection/>
|
||||
</Form.Field>
|
||||
|
||||
<Button type='submit' onClick={this.saveChanges}>Save</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LightForm extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
render() {
|
||||
let options = [
|
||||
{
|
||||
key: 1,
|
||||
value: "common",
|
||||
text: "Normal Light"
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
value: "intensity",
|
||||
text: "Supports intensity level"
|
||||
}
|
||||
]
|
||||
return (
|
||||
<Form.Field>
|
||||
<label style={{color: "white"}}>Type of light</label>
|
||||
<Dropdown clearable options={options} selection/>
|
||||
</Form.Field>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export default class NewDevice extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
openForm: false
|
||||
}
|
||||
}
|
||||
|
||||
onClickDevice = (event) => {
|
||||
console.log(this.props.children);
|
||||
this.setState((prevState) => ({openForm: !prevState.openForm}));
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<StyledDiv onClick={this.onClickDevice} style={{textAlign: "center"}}>
|
||||
<Image src="/img/add.svg" style={{filter: "invert()"}}/>
|
||||
{this.state.openForm ? (
|
||||
<NewDeviceForm/>
|
||||
) : ""}
|
||||
</StyledDiv>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,67 +1,56 @@
|
|||
import React, {useState} from "react";
|
||||
import {
|
||||
CircularInput,
|
||||
CircularTrack,
|
||||
CircularProgress,
|
||||
CircularThumb,
|
||||
useCircularInputContext,
|
||||
} from 'react-circular-input'
|
||||
/**
|
||||
* Users can add sensors in their rooms.
|
||||
* Sensors typically measure physical quantities in a room.
|
||||
* You must support temperature sensors, humidity sensors, light sensors (which measure luminosity1).
|
||||
* Sensors have an internal state that cannot be changed by the user.
|
||||
* For this story, make the sensors return a constant value with some small random error.
|
||||
*/
|
||||
|
||||
import React, {Component} from "react";
|
||||
import {CircularInput, CircularProgress, CircularTrack} from "react-circular-input";
|
||||
import {errorStyle, sensorText, style, valueStyle} from "./SensorStyle";
|
||||
|
||||
// Example of a custom component to display text on top of the thumb
|
||||
|
||||
function TemperatureDisplay() {
|
||||
const {getPointFromValue, value} = useCircularInputContext();
|
||||
const {x, y} = getPointFromValue();
|
||||
const style = {
|
||||
fontFamily: "Lato",
|
||||
fontSize: "1.2rem",
|
||||
color: "white",
|
||||
export default class Light extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
turnedOn: false,
|
||||
value: 20,
|
||||
error : 2.4
|
||||
};
|
||||
this.units = "ºC"
|
||||
}
|
||||
|
||||
setName = () => {
|
||||
if(this.props.device.name.length > 15){
|
||||
return this.props.device.name.slice(0,12) + "..."
|
||||
}
|
||||
return this.props.device.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<text x={x} y={y} style={style}>
|
||||
{Math.round(value * 100)}
|
||||
</text>
|
||||
)
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
export default function Sensor(props) {
|
||||
const style = {width: "10rem", height: "10rem", position: "absolute", top: "0", left: "0"};
|
||||
const valueStyle = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "2.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
}
|
||||
|
||||
const nameStyle = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "1.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
}
|
||||
const [value, setValue] = useState(0.25);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CircularInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
value={this.state.value/100}
|
||||
style={style}
|
||||
>
|
||||
<CircularTrack/>
|
||||
<CircularProgress/>
|
||||
<CircularThumb/>
|
||||
|
||||
<text style={valueStyle} x={100} y={100} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{Math.round(value * props.device.maxValue)}{props.device.units}
|
||||
<text style={valueStyle} x={100} y={80} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{Math.round(this.state.value)}{this.units}
|
||||
</text>
|
||||
<text style={nameStyle} x={100} y={150} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{props.device.name}
|
||||
<text style={errorStyle} x={100} y={100} textAnchor="middle" dy="0.6em" fontWeight="bold">
|
||||
±{this.state.error}
|
||||
</text>
|
||||
<text style={sensorText} x={100} y={150} textAnchor="middle" dy="0.3em" fontWeight="bold">
|
||||
{this.setName()}
|
||||
</text>
|
||||
</CircularInput>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
23
smart-hut/src/components/dashboard/devices/SensorStyle.js
Normal file
23
smart-hut/src/components/dashboard/devices/SensorStyle.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
export const style = {width: "10rem", height: "10rem", position: "absolute", top: "0", left: "0"};
|
||||
|
||||
export const sensorText = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "1.2rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
}
|
||||
|
||||
export const valueStyle = {
|
||||
fill: "#3e99ff",
|
||||
fontSize: "2.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
};
|
||||
|
||||
|
||||
export const errorStyle = {
|
||||
fill: "#ff4050",
|
||||
fontSize: "1.5rem",
|
||||
fontFamily: "Lato",
|
||||
textShadow: "1px 1px 0.5px rgba(0, 0, 0, .2)",
|
||||
}
|
60
smart-hut/src/components/dashboard/devices/SmartPlug.js
Normal file
60
smart-hut/src/components/dashboard/devices/SmartPlug.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
|
||||
A smart plug is a plug that has a boolean internal state, i.e., that can be turned on or off, either with the
|
||||
SmartHut interface or by a switch.
|
||||
The smart plug also stores the total energy consumed while the plug is active, in terms of kilowatt-hours
|
||||
2
|
||||
(kWh) . The user can reset this value.
|
||||
|
||||
**/
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import {iconStyle, nameStyle, StyledDiv} from "./styleComponents";
|
||||
import Settings from "./DeviceSettings";
|
||||
import {Image} from "semantic-ui-react";
|
||||
import {energyConsumedStyle, imageStyle} from "./SmartPlugStyle";
|
||||
|
||||
export default class SmartPlug extends Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
turnedOn: false,
|
||||
energyConsumed : 0 // kWh
|
||||
};
|
||||
this.iconOn = "/img/smart-plug.svg";
|
||||
this.iconOff = "/img/smart-plug-off.svg"
|
||||
}
|
||||
|
||||
onClickDevice = () => {
|
||||
this.setState((prevState) => ({turnedOn: !prevState.turnedOn}));
|
||||
};
|
||||
|
||||
getIcon = () => {
|
||||
if(this.state.turnedOn){
|
||||
return this.iconOn;
|
||||
}
|
||||
return this.iconOff;
|
||||
};
|
||||
|
||||
resetEnergyConsumedValue = () => {
|
||||
// In the settings form there must be an option to restore this value
|
||||
// along with the rename feature.
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<StyledDiv onClick={this.props.edit ? () => {} : this.onClickDevice} style={{textAlign: "center"}}>
|
||||
<Settings
|
||||
deviceId={this.props.device.id}
|
||||
edit={this.props.edit}
|
||||
onChangeData={(id, newSettings) => this.props.onChangeData(id, newSettings)}/>
|
||||
<Image src={this.getIcon()} style={imageStyle}/>
|
||||
<h4 style={energyConsumedStyle}>{this.state.energyConsumed} KWh</h4>
|
||||
<h5 style={nameStyle}>{this.props.device.name}</h5>
|
||||
</StyledDiv>
|
||||
)
|
||||
}
|
||||
}
|
19
smart-hut/src/components/dashboard/devices/SmartPlugStyle.js
Normal file
19
smart-hut/src/components/dashboard/devices/SmartPlugStyle.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {iconStyle} from "./styleComponents";
|
||||
|
||||
export const energyConsumedStyle = {
|
||||
fontSize : "1.3rem",
|
||||
position: "absolute",
|
||||
top: "30%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
};
|
||||
|
||||
export const imageStyle = {
|
||||
width: "3.5rem",
|
||||
height: "auto",
|
||||
position: "absolute",
|
||||
top: "10%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
filter: "drop-shadow( 1px 1px 0.5px rgba(0, 0, 0, .25))"
|
||||
};
|
0
smart-hut/src/components/dashboard/devices/Switcher.js
Normal file
0
smart-hut/src/components/dashboard/devices/Switcher.js
Normal file
|
@ -1,13 +1,18 @@
|
|||
export const LightDevice = {
|
||||
type : "light",
|
||||
img : "/img/lightOff.svg",
|
||||
imgClick : "/img/lightOn.svg"
|
||||
};
|
||||
|
||||
export const SmartPlugDevice = {
|
||||
img : "/img/smart-plug.svg",
|
||||
imgClick : "/img/smart-plug-off.svg"
|
||||
};
|
||||
|
||||
export const TemperatureSensor = {
|
||||
type : "temperature_sensor",
|
||||
img : "",
|
||||
imgClick : "",
|
||||
units: "ºC",
|
||||
maxValue : 30
|
||||
};
|
||||
|
||||
export const deviceList = ["Light", "Dimmer", "Switcher", "Smart Plug", "Sensor"];
|
||||
|
|
5
smart-hut/src/components/dashboard/devices/constants.js
Normal file
5
smart-hut/src/components/dashboard/devices/constants.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const DEVICE_NAME_MAX_LENGTH = 15;
|
||||
|
||||
export function checkMaxLength(name){
|
||||
return !(name.length > DEVICE_NAME_MAX_LENGTH);
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
export const editButtonStyle = {
|
||||
position : "absolute",
|
||||
top: "0",
|
||||
|
@ -12,6 +14,7 @@ export const editButtonStyle = {
|
|||
};
|
||||
|
||||
export const panelStyle = {
|
||||
position : "relative",
|
||||
backgroundColor: "#fafafa",
|
||||
height: "100%",
|
||||
width: "auto",
|
||||
|
@ -38,4 +41,58 @@ export const editModeIconStyle = {
|
|||
height : "0.75rem",
|
||||
borderRadius : "20%",
|
||||
zIndex : "101"
|
||||
};
|
||||
|
||||
export const iconStyle = {
|
||||
width: "4rem",
|
||||
height: "auto",
|
||||
position: "absolute",
|
||||
top: "20%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
filter: "drop-shadow( 1px 1px 0.5px rgba(0, 0, 0, .25))"
|
||||
};
|
||||
export const nameStyle = {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
};
|
||||
|
||||
|
||||
export const formStyle = {
|
||||
position : "absolute",
|
||||
zIndex: "1000",
|
||||
width : "80rem",
|
||||
height : "10rem",
|
||||
padding : "1rem",
|
||||
margin : "1rem",
|
||||
borderRadius : "10%",
|
||||
boxShadow : "1px 1px 5px 2px #5d5d5d",
|
||||
backgroundColor: "#3e99ff",
|
||||
};
|
||||
|
||||
export const addDeviceFormStyle = {
|
||||
maxWidth : "400px",
|
||||
background : "#3e99ff",
|
||||
paddingRight : "5rem",
|
||||
};
|
||||
|
||||
export const StyledDiv = styled.div`
|
||||
background-color : white;
|
||||
padding : 3rem;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
border-radius : 100%;
|
||||
border : none;
|
||||
position : relative;
|
||||
box-shadow: 3px 2px 10px 5px #ccc;
|
||||
transition : all .3s ease-out;
|
||||
:hover{
|
||||
background-color : #f2f2f2;
|
||||
}
|
||||
:active{
|
||||
transform : translate(0.3px, 0.8px);
|
||||
box-shadow: 0.5px 0.5px 7px 3.5px #ccc;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -4,18 +4,24 @@ import Navbar from './Navbar'
|
|||
import MyHeader from '../components/HeaderController'
|
||||
|
||||
import { call } from '../client_server';
|
||||
import {Button} from 'semantic-ui-react';
|
||||
import { Menu } from 'semantic-ui-react'
|
||||
import { Grid, Image, Icon } from 'semantic-ui-react'
|
||||
import { Grid } from 'semantic-ui-react'
|
||||
/*
|
||||
rooms -> actual rooms
|
||||
activeItem -> the current room in view
|
||||
devices -> current device in current room view
|
||||
|
||||
|
||||
id of Home is -1
|
||||
*/
|
||||
|
||||
export default class Dashboard extends Component{
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
rooms: [],
|
||||
activeItem: "Home",
|
||||
tkn: this.props.tkn
|
||||
activeItem: -1,
|
||||
devices: [],
|
||||
tkn: this.props.tkn,
|
||||
};
|
||||
|
||||
this.addRoom = this.addRoom.bind(this);
|
||||
|
@ -33,6 +39,17 @@ export default class Dashboard extends Component{
|
|||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
call.getAllDevices(this.props.tkn)
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
res.data.forEach((e) => {
|
||||
this.setState(state => ({
|
||||
devices: state.devices.concat([e])
|
||||
}));
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
addRoom(data) {
|
||||
|
@ -50,11 +67,27 @@ export default class Dashboard extends Component{
|
|||
};
|
||||
|
||||
deleteRoom(id) {
|
||||
|
||||
call.deleteRoom(id)
|
||||
.then(res => {
|
||||
//remove room in state.rooms
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
handleItemClick(el) {
|
||||
// el -> obj of name and id
|
||||
//da fare richiesta get della room e settare activeItem
|
||||
call.getAllDevicesByRoom(el.id, this.props.tkn)
|
||||
.then(res => {
|
||||
res.data.forEach((e) => {
|
||||
this.setState(state => ({
|
||||
devices: state.devices.concat([e])
|
||||
}));
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -72,7 +105,7 @@ export default class Dashboard extends Component{
|
|||
</Grid.Column>
|
||||
|
||||
<Grid.Column width={13}>
|
||||
<DevicePanel />
|
||||
<DevicePanel devices={this.state.devices} />
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, {Component} from 'react';
|
||||
import { Message} from 'semantic-ui-react';
|
||||
import { Header, Grid, Message, Button} from 'semantic-ui-react';
|
||||
import {Link } from "react-router-dom";
|
||||
|
||||
|
||||
|
@ -7,13 +7,29 @@ export default class FourOhFour extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<div style={{height : "110vh", background: '#1b1c1d'}}>
|
||||
<Grid centered>
|
||||
<Grid.Row>
|
||||
<Header as='h1'>404</Header>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={10}>
|
||||
<Message>
|
||||
<Message.Header>404 Page Not Found</Message.Header>
|
||||
<p>
|
||||
Hey what are you doing here?
|
||||
Go back to our homepage <Link to="/"/>
|
||||
Looks like you are lost, this room does not exist.
|
||||
</p>
|
||||
</Message>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={6}>
|
||||
<Button >
|
||||
<Link to="/">Go back to our main room</Link>
|
||||
</Button>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue