/* Copyright 2018 Joseph Weston This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React from 'react' ; import {SlideDown} from 'react-slidedown' import Spinner from 'react-spinkit' import { ComposableMap, ZoomableGroup, Geographies, Geography, } from "react-simple-maps" import 'bulma/css/bulma.css' ; import 'react-slidedown/lib/slidedown.css' // Import licence for GEOJSON so as to include it into the webpack output. // This is necessary to avoid violating the license terms. import './static/LICENSE' import World from './static/world.geo.json' const VPN = Object.freeze({ disconnected:1, connecting:2, connected:3, disconnecting: 4, }) ; class Nord extends React.Component { constructor(props) { super(props) ; this.state = { enabled: false, loading: true, status: VPN.disconnected, country: null, host: null, } ; this.connection = null this.connect = this.connect.bind(this) ; this.disconnect = this.disconnect.bind(this) ; this.handleData = this.handleData.bind(this) ; } componentDidMount() { this.setState({enabled: false, loading: true}) this.connection = new WebSocket('ws://127.0.0.1:5000/api') ; this.connection.onopen = () => this.setState({enabled: true, loading: false}) ; this.connection.onerror = () => this.setState({enabled: false, loading: false}) ; this.connection.onclose = () => this.setState({enabled: false, loading: false}) ; this.connection.onmessage = this.handleData ; } handleData(msg) { msg = JSON.parse(msg.data) switch(msg.state) { case "connected": this.setState({status: VPN.connected, host: msg.host}) ; break ; case "disconnected": this.setState({status: VPN.disconnected}) ; break ; } } connect(geography) { const country_info = geography.properties ; this.setState({ status: VPN.connecting, country: country_info.NAME, host: null, }) ; this.connection.send(JSON.stringify({ method: 'connect', country: country_info.ISO_A2 })) ; } disconnect() { this.setState((prev) => ({ ...prev, status: VPN.disconnecting })) ; this.connection.send(JSON.stringify({ method: 'disconnect' })) ; } render() { return ( <div> <Title/> <VPNStatus connection={ this.state } onDisconnect={ this.disconnect }/> <WorldMap onClick={ this.connect }/> </div> ) ; } } const Title = () => ( <section className="hero is-primary is-info has-text-centered"> <div className="container"> <div className="is-size-1"> Nord </div> </div> </section> ) ; const VPNStatus = (props) => { const connection = props.connection ; const ConnectionSpinner = () => ( <div style={{"display": "inline-block", "margin-bottom": "-5px"}}> <Spinner name="double-bounce" /> </div> ) ; const DisconnectButton = () => { var classes = "button is-danger " ; if (connection.status == VPN.disconnecting) { classes += "is-loading" } return ( <button className={classes} onClick={ props.onDisconnect }> Disconnect </button> ) ; } const style = {"margin-right": "10px", display: "inline"} ; var color = "" ; var text = "" ; switch(props.connection.status) { case VPN.disconnected: break ; case VPN.connecting: color = "is-warning" ; text = <div> <div style={style}> Connecting to servers in {connection.country} </div> <ConnectionSpinner/> </div> break ; case VPN.connected: case VPN.disconnecting: color = "is-success" ; text = (<div> <div style={style}> Connected to {connection.host} </div> <DisconnectButton/> </div>) ; break ; } if (!connection.enabled && !connection.loading) { color = "is-danger" text = <div style={style}>Lost connection to server</div> } const classes = "hero is-primary has-text-centered " + color return ( <SlideDown> <section className={classes}> <div className="container"> <div className="is-size-4"> {text} </div> </div> </section> </SlideDown> ) ; } ; const WorldMap = (props) => { const zoom = 2 ; const center = [ 0, 30 ] ; const default_map_style = { fill: "#ECEFF1", stroke: "#607D8B", strokeWidth: 0.75, outline: "none", } ; const map_container_style = { width: "100%", height: "auto", } ; const map_style = { default: default_map_style, hover: { ...default_map_style, fill: "#CFD8DC" }, pressed: { ...default_map_style, fill: "#CFD8DC" }, } ; return ( <ComposableMap style={ map_container_style }> <ZoomableGroup zoom={ zoom } center={ center }> <Geographies geography={ World }> {(geographies, projection) => geographies.map(geography => ( <Geography key={ geography.id } geography={ geography } projection={ projection } style={ map_style } onClick={ props.onClick } /> ))} </Geographies> </ZoomableGroup> </ComposableMap> ) ; } ; export default Nord ;