Browse Source

Initial commit

Yann Weber 2 years ago
commit
93bd1e138a
20 changed files with 922 additions and 0 deletions
  1. 24
    0
      .gitignore
  2. 50
    0
      README.md
  3. 1
    0
      config.json
  4. 34
    0
      package.json
  5. 22
    0
      public/index.html
  6. 44
    0
      src/Alarm.css
  7. 197
    0
      src/Alarm.js
  8. 78
    0
      src/AlarmNotif.js
  9. 14
    0
      src/Alarms.css
  10. 94
    0
      src/Alarms.js
  11. 4
    0
      src/Clock.css
  12. 216
    0
      src/Clock.js
  13. 27
    0
      src/ClockDisp.css
  14. 56
    0
      src/ClockDisp.js
  15. 1
    0
      src/config.json
  16. 10
    0
      src/index.css
  17. 13
    0
      src/index.js
  18. 1
    0
      src/react-app-env.d.ts
  19. 10
    0
      src/utils.js
  20. 26
    0
      tsconfig.json

+ 24
- 0
.gitignore View File

@@ -0,0 +1,24 @@
1
+# dependencies
2
+/node_modules
3
+/.pnp
4
+.pnp.js
5
+
6
+# testing
7
+/coverage
8
+
9
+# production
10
+/build
11
+
12
+# vim
13
+.*.swp
14
+
15
+# misc
16
+.env.local
17
+.env.development.local
18
+.env.test.local
19
+.env.production.local
20
+
21
+npm-debug.log*
22
+yarn-debug.log*
23
+yarn-error.log*
24
+

+ 50
- 0
README.md View File

@@ -0,0 +1,50 @@
1
+# Clock websocket React/TS UI
2
+
3
+## Deployment
4
+
5
+### Configure & build
6
+
7
+Install dependencies
8
+`npm install`
9
+
10
+Change the `WS_URL` value according to server deployment configuration.
11
+`edit src/config.json`
12
+
13
+Create an optimized production build
14
+`npm run build`
15
+
16
+Copy `./build` to webserver's (`/var/www/clock`).
17
+
18
+### Nginx webserver configuration
19
+
20
+Install nginx
21
+`apt install nginx`
22
+
23
+Create server configuration
24
+```
25
+rm /etc/nginx/site-enabled/default
26
+edit /etc/nginx/sites-available/clock
27
+ln -s /etc/nginx/sites-available/clock /etc/nginx/sites-enabled/
28
+```
29
+
30
+*Example server config :*
31
+```
32
+server {
33
+	listen 80 default_server;
34
+	listen [::]:80 default_server;
35
+
36
+	root /var/www/clock/;
37
+
38
+	index index.html;
39
+
40
+	server_name _;
41
+
42
+	location / {
43
+		try_files $uri $uri/ =404;
44
+	}
45
+}
46
+```
47
+
48
+## Start a development server
49
+
50
+`npm run start`

+ 1
- 0
config.json View File

@@ -0,0 +1 @@
1
+src/config.json

+ 34
- 0
package.json View File

@@ -0,0 +1,34 @@
1
+{
2
+  "name": "Clock",
3
+  "private": true,
4
+  "version": "1.0.0",
5
+  "description": "",
6
+  "main": "index.js",
7
+  "scripts": {
8
+    "start": "react-scripts start",
9
+    "build": "react-scripts build"
10
+  },
11
+  "author": "",
12
+  "license": "ISC",
13
+  "dependencies": {
14
+    "react": "^17.0.2",
15
+    "react-dom": "^17.0.2"
16
+  },
17
+  "devDependencies": {
18
+    "@types/react": "^17.0.35",
19
+    "@types/react-dom": "^17.0.11",
20
+    "react-scripts": "^4.0.3"
21
+  },
22
+  "browserslist": {
23
+    "production": [
24
+      ">0.2%",
25
+      "not dead",
26
+      "not op_mini all"
27
+    ],
28
+    "development": [
29
+      "last 1 chrome version",
30
+      "last 1 firefox version",
31
+      "last 1 safari version"
32
+    ]
33
+  }
34
+}

+ 22
- 0
public/index.html View File

@@ -0,0 +1,22 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+  <head>
4
+    <meta charset="utf-8" />
5
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+    <!--
8
+      Notice the use of %PUBLIC_URL% in the tags above.
9
+      It will be replaced with the URL of the `public` folder during the build.
10
+      Only files inside the `public` folder can be referenced from the HTML.
11
+
12
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
13
+      work correctly both with client-side routing and a non-root public URL.
14
+      Learn how to configure a non-root public URL by running `npm run build`.
15
+    -->
16
+    <title>Clock</title>
17
+  </head>
18
+  <body>
19
+    <noscript>You need to enable JavaScript to run this app.</noscript>
20
+    <div id="root"></div>
21
+  </body>
22
+</html>

+ 44
- 0
src/Alarm.css View File

@@ -0,0 +1,44 @@
1
+.AlrmRinging {
2
+	color: #E22;
3
+}
4
+
5
+.Alrm::before {
6
+	content: "\2022";
7
+	font-weight: bold;
8
+	display: inline-block;
9
+	width: 1em;
10
+	vertical-align: -.25em;
11
+	border-radius: 70%;
12
+}
13
+
14
+.AlrmRinging::before {
15
+	color: #DD0;
16
+	background-color: #E12;
17
+}
18
+
19
+.AlrmOn::before {
20
+	/*
21
+	color: #84A;
22
+	background-color: #5D3;
23
+	*/
24
+	color: #3A3;
25
+	background-color: #FB4;
26
+}
27
+
28
+.AlrmOff::before {
29
+	color: #777;
30
+	background-color: #CCC;
31
+}
32
+
33
+.AlrmSetTimeForm {
34
+	display: inline;
35
+}
36
+
37
+.AlrmStateBtn {
38
+	text-align: left;
39
+}
40
+
41
+.AlrmDeleteBtn {
42
+	border-radius: 15px;
43
+	width: 2em;
44
+}

+ 197
- 0
src/Alarm.js View File

@@ -0,0 +1,197 @@
1
+import * as React from 'react';
2
+
3
+import util from './utils';
4
+import Notif from './AlarmNotif';
5
+
6
+import './Alarm.css';
7
+
8
+/* An alarm component : represent an alarm in the Alarms table */
9
+class Alarm extends React.Component {
10
+
11
+	constructor(props:{	name:string, // alarm name
12
+				time:string, // alarm ring time
13
+				on:bool, // on flag
14
+				ringing:bool, // ringing flag
15
+				snooze:bool, // snooze flag
16
+				ws:WebSocket, // Clock.state.ws reference
17
+				upd:function, // Clock.alrmUpdate() reference
18
+	}) {
19
+		super(props)
20
+
21
+		this.time_input = null;
22
+		this.state = {	new_time:props.time, // time input value
23
+				prev_new_time:props.time, // previous input value
24
+				set_time:false, // if true show the time input
25
+		}
26
+		
27
+		this.handleChangeSetTime = this.handleChangeSetTime.bind(this)
28
+		this.set_time = this.set_time.bind(this)
29
+		this.on = this.on.bind(this)
30
+		this.off = this.off.bind(this)
31
+		this.snooze = this.snooze.bind(this)
32
+		this.Delete = this.Delete.bind(this)
33
+	}
34
+
35
+	render() {
36
+		let alrm_state_btn = <></>;
37
+		let alrm_class = 'Alrm';
38
+
39
+		if(this.props.on) {
40
+			alrm_state_btn = (
41
+				<button
42
+					onClick={this.off}
43
+				>
44
+					<abbr
45
+						title="Turn OFF"
46
+					>off</abbr>
47
+				</button>
48
+			);
49
+			if(this.props.ringing) {
50
+				alrm_state_btn = <>{alrm_state_btn}<button onClick={this.snooze}>snooze</button></>
51
+				alrm_class += ' AlrmRinging';
52
+			} else {
53
+				alrm_class += ' AlrmOn';
54
+			}
55
+		} else {
56
+			alrm_class += ' AlrmOff';
57
+			alrm_state_btn = (
58
+				<>
59
+				<button
60
+					onClick={this.on}
61
+				>
62
+					<abbr
63
+						title="Turn ON"
64
+					>on</abbr>
65
+				</button>
66
+				</>
67
+			);
68
+		}
69
+
70
+		let snooze = this.props.snooze?<span className="snooze">Snooze</span>:<></>;
71
+
72
+		let set_lbl = <abbr title="Set ringtime">Set</abbr>;
73
+		let set_id = 'ringtime'+this.props.name;
74
+		let time_disp = this.state.set_time ? (
75
+			<form
76
+				onSubmit={this.handleSubmitSetTime}
77
+				className="AlrmSetTimeForm"
78
+			>
79
+				<input
80
+					id={set_id}
81
+					onChange={this.handleChangeSetTime}
82
+					value={this.state.new_time}
83
+					style={{width:"5em"}}
84
+					ref={(input  => {this.time_input = input;})}
85
+				/>
86
+				<button
87
+					onClick={this.set_time}
88
+				>{set_lbl}</button>
89
+			</form>
90
+		):(
91
+			<span>
92
+				{this.props.time}&nbsp;
93
+				<button
94
+					onClick={this.set_time}
95
+				>{set_lbl}</button>
96
+			</span>
97
+		);
98
+
99
+		return (
100
+			<tr className={alrm_class}>
101
+				<td className="AlrmName">{this.props.name}</td>
102
+				<td>
103
+					{time_disp}
104
+				</td>
105
+				<td className="AlrmStateBtn">
106
+					{alrm_state_btn}
107
+					{snooze}
108
+				</td>
109
+				<td>
110
+					<button
111
+						className="AlrmDeleteBtn"
112
+						onClick={this.Delete}
113
+					>
114
+						<abbr title="Delete alarm">X</abbr>
115
+					</button>
116
+				</td>
117
+			</tr>
118
+		);
119
+	}
120
+
121
+	componentDidUpdate() {
122
+		if(this.time_input !== null) {
123
+			// Force focus on the edited alarm on each update
124
+			this.time_input.focus()
125
+		}
126
+	}
127
+
128
+	/* Time input form submit handler */
129
+	set_time (evt) {
130
+		evt.preventDefault()
131
+		if(this.state.set_time) {
132
+			this.props.ws.send('alarm set '+util.escape_arg(this.props.name)+' '+this.state.new_time.replace(/:/g, ' '))
133
+			this.props.upd()
134
+		}
135
+		this.setState({new_time:this.props.time,
136
+			       set_time: !this.state.set_time})
137
+	}
138
+
139
+	/* Time input change event handler */
140
+	handleChangeSetTime (evt) {
141
+		let val = evt.target.value
142
+		let prev = evt.target.value
143
+		let prevdel = this.state.prev_new_time.length > val.length
144
+		if(val.length > 0) {
145
+			if(!val.match(/.*[0-9:]$/)) {
146
+				val = val.substring(0, val.length-1)
147
+			}
148
+			if(!prevdel && (
149
+				val.match(/^[0-9]{2}$/) ||
150
+				val.match(/^[0-9]{1,2}:[0-9]{2}$/)
151
+			)) {
152
+				val += ':'
153
+				prev = val
154
+			}
155
+			if(val.length > 8 || val.match(/[^0-9:]/) || val.match(/^.*:.*:.*:.*$/)) {
156
+				prev = ''
157
+				val = ''
158
+			}
159
+		}
160
+		this.setState({new_time:val, prev_new_time:prev})
161
+	}
162
+
163
+	/* on button click handler  */
164
+	on (evt) {
165
+		Notif.requestPermission()
166
+		this.props.ws.send('alarm on '+util.escape_arg(this.props.name))
167
+		this.props.upd()
168
+	}
169
+
170
+	/* off button click handler */
171
+	off (evt) {
172
+		this.props.ws.send('alarm off '+util.escape_arg(this.props.name))
173
+		this.props.upd()
174
+	}
175
+
176
+	/* snooze button click handler */
177
+	snooze (evt) {
178
+		this.props.ws.send('alarm snooze '+util.escape_arg(this.props.name)+' 1')
179
+		this.props.upd()
180
+	}
181
+
182
+	/* delete button click handler */
183
+	Delete (evt) {
184
+		var todel = true
185
+		if(this.props.ringing || this.props.snooze || this.props.on) {
186
+			todel = confirm('Do your really want to delete "'+this.props.name+'" ?')
187
+		}
188
+		if(todel) {
189
+			this.props.ws.send('alarm del '+util.escape_arg(this.props.name))
190
+			this.props.upd()
191
+		}
192
+	}
193
+
194
+}
195
+
196
+
197
+export default Alarm;

+ 78
- 0
src/AlarmNotif.js View File

@@ -0,0 +1,78 @@
1
+/* Small set of function for Notification handling */
2
+
3
+/* Stores Notifications instance associated with alarm name */
4
+var _notifications = {};
5
+var _requested = false;
6
+
7
+/* Return True if the browser supports Notifications
8
+ * Note : Return False in case user denied the permission to notify
9
+ */
10
+function support() {
11
+	return "Notification" in window && Notification.permission !== 'denied'
12
+}
13
+
14
+/* Return True if Notifications are supported and permission granted */
15
+function granted() {
16
+	if(!support()) {
17
+		return false;
18
+	}
19
+	return Notification.permission === 'granted'
20
+}
21
+
22
+/* Request permission for notifications */
23
+function requestPermission() {
24
+	if(_requested) { return; }
25
+	_requested = true
26
+	Notification.requestPermission()
27
+}
28
+
29
+/* Given an alarm name and a ringtime, create a new notification
30
+ * for the ringing alarm if none existing
31
+ */
32
+function set_notif_alrm(name:string, time_str:string) {
33
+	if(!support()) { return; }
34
+	if(!granted()) {
35
+		if(_requested) { return; } // allready requested
36
+		_requested = true
37
+		Notification.requestPermission().then(function (perm) {
38
+			set_notif_alrm(name)
39
+		})
40
+	}
41
+	if(name in _notifications) { return; }
42
+
43
+	let notif = new Notification(time_str+" '"+name+"'",
44
+				{body:time_str+' alarm "'+name+'" is ringing.'})
45
+	
46
+	notif.addEventListener('close', () => {
47
+		if(!name in _notifications) {
48
+			delete _notifications[name];
49
+		}
50
+	});
51
+
52
+	notif.addEventListener('error', () => {
53
+		if(!name in _notifications) {
54
+			delete _notifications[name];
55
+		}
56
+	});
57
+
58
+	_notifications[name] = notif;
59
+}
60
+
61
+/* Given an alarm name, delete & close associated Notification if one exists */
62
+function unset_notif_alrm(name:string) {
63
+	if(!support()) { return; }
64
+	if(name in _notifications) {
65
+		_notifications[name].close()
66
+		delete _notifications[name]
67
+	}
68
+}
69
+
70
+const Notif = {
71
+
72
+	set: set_notif_alrm,
73
+	unset: unset_notif_alrm,
74
+	requestPermission: requestPermission,
75
+
76
+};
77
+
78
+export default Notif;

+ 14
- 0
src/Alarms.css View File

@@ -0,0 +1,14 @@
1
+.Alarms {
2
+	text-align: center;
3
+}
4
+
5
+.AlrmList {
6
+	margin: auto;
7
+	padding: 1%;
8
+}
9
+
10
+.AlrmList input, .AlrmList button {
11
+	font-family: monospace, monospace;
12
+}
13
+
14
+

+ 94
- 0
src/Alarms.js View File

@@ -0,0 +1,94 @@
1
+import * as React from 'react';
2
+
3
+import util from'./utils';
4
+import Alarm from './Alarm';
5
+
6
+import './Alarms.css';
7
+
8
+/* An alarm list component : a table with Alarm component instances */
9
+class Alarms extends React.Component {
10
+	constructor(props:{	alarms:list, // List of alarm informations
11
+				ws:WebSocket, // Clock.state.ws reference
12
+				alrmUpd:function // Clock.alrmUpdate() reference
13
+	}) {
14
+		super(props);
15
+		this.state = {	new_alrm:'', // new alarm input value
16
+				add_alrm:false // if true show new alarm input
17
+		}
18
+		this.handleNewAlarm = this.handleNewAlarm.bind(this)
19
+		this.handleNewAlarmChange = this.handleNewAlarmChange.bind(this)
20
+	}
21
+
22
+	render() {
23
+		let add_alrm_lbl = 'New alarm'
24
+		let add_alrm = (
25
+			<button
26
+				onClick={this.handleNewAlarm}
27
+			>{add_alrm_lbl}</button>);
28
+		if(this.state.add_alrm) {
29
+			add_alrm = (
30
+				<form onSubmit={this.handleNewAlarm}>
31
+					<input
32
+						type="submit"
33
+						value={add_alrm_lbl}
34
+					/>
35
+					<label
36
+						htmlFor="new-alrm"
37
+					>
38
+						New alarm name
39
+					</label>
40
+					<input
41
+						id="new-alrm"
42
+						onChange={this.handleNewAlarmChange}
43
+						value={this.state.new_alrm}
44
+					/>
45
+				</form>
46
+			);
47
+		}
48
+		return (
49
+			<div className="Alarms">
50
+				<table className="AlrmList">
51
+				<tbody>
52
+				{Object.entries(this.props.alarms).map(alrm => (
53
+					<Alarm
54
+						key={alrm[0]}
55
+						name={alrm[0]}
56
+						time={alrm[1].time}
57
+						on={alrm[1].on}
58
+						ringing={alrm[1].ringing}
59
+						snooze={alrm[1].snooze}
60
+						ws={this.props.ws}
61
+						upd={this.props.alrmUpd}
62
+					/>
63
+				))}
64
+				</tbody>
65
+				</table>
66
+				{add_alrm}
67
+			</div>
68
+		);
69
+	}
70
+
71
+	/* New alarm input form submit handler */
72
+	handleNewAlarm(evt) {
73
+		evt.preventDefault();
74
+		if(!this.state.add_alrm) {
75
+			this.setState({add_alrm:true})
76
+			return
77
+		}
78
+		if(this.state.new_alrm.length == 0) {
79
+			this.setState({add_alrm:false})
80
+			return;
81
+		}
82
+		let esc_name = util.escape_arg(this.state.new_alrm)
83
+		this.props.ws.send('alarm add '+esc_name)
84
+		this.setState({new_alrm:'', add_alrm:false})
85
+		this.props.alrmUpd()
86
+	}
87
+
88
+	/* New alarm input change handler */
89
+	handleNewAlarmChange(evt) {
90
+		this.setState({new_alrm:evt.target.value})
91
+	}
92
+}
93
+
94
+export default Alarms;

+ 4
- 0
src/Clock.css View File

@@ -0,0 +1,4 @@
1
+.NewTz {
2
+	text-align: center;
3
+}
4
+

+ 216
- 0
src/Clock.js View File

@@ -0,0 +1,216 @@
1
+import * as React from 'react';
2
+
3
+import util from './utils';
4
+import Notif from './AlarmNotif';
5
+import Alarms from './Alarms';
6
+import ClockDisp from './ClockDisp';
7
+
8
+import './Clock.css';
9
+
10
+/* A clock UI for a websocket server handling timezones & alarms */
11
+class Clock extends React.Component {
12
+	constructor(props:{	wsurl:string // Websocket URL
13
+	}) {
14
+		super(props)
15
+		
16
+		// Checking cookies in order to find an existing SESSION
17
+		let decoded = '; '+decodeURIComponent(document.cookie)
18
+		let cookies = decoded.split('; SESSION=')
19
+		let session_id = 'noid'
20
+		if(cookies.length == 2) {
21
+			session_id = cookies[1].split(';')[0]
22
+		}
23
+
24
+		this.state = {ws: null, // Stores the websocket instance
25
+			time: '', // Stores time received from ws
26
+			changetz: false, // True when change tz form displayed
27
+			tzname: '', // The current tz name
28
+			new_tz: '', // tz form input value
29
+
30
+			alarms: {}, // alarms received from ws
31
+			err: '', //  error zone content
32
+			session_id: session_id, // session id 
33
+		}
34
+
35
+		this.handleSubmitTz = this.handleSubmitTz.bind(this)
36
+		this.handleChangeTz = this.handleChangeTz.bind(this)
37
+		this.alrmUpdate = this.alrmUpdate.bind(this)
38
+		this.wsConnect = this.wsConnect.bind(this)
39
+
40
+	}
41
+
42
+	render() {
43
+		let newtz_lbl = 'Change timezone';
44
+		let newtz = <button onClick={this.handleSubmitTz}>{newtz_lbl}</button>;
45
+
46
+		if(this.state.changetz) {
47
+			newtz = (
48
+			<form onSubmit={this.handleSubmitTz}>
49
+				<input
50
+					type="submit"
51
+					value={newtz_lbl}
52
+				/>
53
+				<br/>
54
+				<label
55
+					htmlFor="new-tz"
56
+				>
57
+					<a
58
+						href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List"
59
+						target="_blank"
60
+					>
61
+						Timezone name
62
+					</a>&nbsp;
63
+				</label>
64
+				<input
65
+					id="new-tz"
66
+					onChange={this.handleChangeTz}
67
+					value={this.state.new_tz}
68
+					placeholder="Continent/City"
69
+				/>
70
+			</form>
71
+			);
72
+		}
73
+		
74
+		return (<>
75
+			<ClockDisp
76
+				time={this.state.time}
77
+				tzname={this.state.tzname}
78
+			/>
79
+			<div className="NewTz">
80
+				{newtz}
81
+			</div>
82
+			<Alarms
83
+				alarms={this.state.alarms}
84
+				ws={this.state.ws}
85
+				alrmUpd={this.alrmUpdate}
86
+			/>
87
+			<p>{this.state.err}</p>
88
+			</>
89
+		);
90
+	}
91
+
92
+	componentDidMount() {
93
+		this.wsConnect() // Connect to websocket after clock mount
94
+	}
95
+
96
+	/* Tz edit input form submit handler */
97
+	handleSubmitTz(evt) {
98
+		evt.preventDefault();
99
+		if(!this.state.changetz) {
100
+			this.setState({changetz:true})
101
+			return;
102
+		}
103
+		if(this.state.new_tz.length == 0) {
104
+			this.setState({changetz:false})
105
+			return;
106
+		}
107
+		this.state.ws.send('tzset '+this.state.new_tz)
108
+		this.setState({changetz:false, new_tz:''})
109
+		this.alrmUpdate();
110
+		return;
111
+	}
112
+
113
+	/* Tz edit input change handler */
114
+	handleChangeTz(evt) {
115
+		this.setState({new_tz:evt.target.value})
116
+	}
117
+
118
+	/* Send an alarm listing request using the websocket
119
+	 * Note : actual update will be done onmessage on ws when an
120
+	 * 	OK:<JSON> message will be received
121
+	 */
122
+	alrmUpdate() {
123
+		this.state.ws.send('alarm list --all')
124
+	}
125
+
126
+	/* Connect/reconnect the websocket */
127
+	wsConnect() {
128
+		try {
129
+			var ws = new WebSocket(this.props.wsurl);
130
+		} catch(expt) {
131
+			if(expt !== null) {
132
+				console.log(expt)
133
+			}
134
+			return this.wsConnect()
135
+		}
136
+
137
+		/* On connection open send the session ID */
138
+		ws.onopen = (evt) => {
139
+			this.state.ws.send(this.state.session_id)
140
+			this.setState({err:''})
141
+		}
142
+
143
+		/* Incoming messages handler
144
+		 * Handles the different type of server messages :
145
+		 * - SESSION:<SESSION_ID> : indicating the session ID to send on next connection
146
+		 * - OK:[<data>] : on command success with data JSON array on alarm listing
147
+		 * - ERR:<REASON> : on command error
148
+		 * - TZN:<TZNAME> : indicating the tz name
149
+		 * - ALRM:<NAME>: indicating an alarm is ringing
150
+		 */
151
+		ws.onmessage = (message_evt) => {
152
+			var msg = message_evt.data
153
+			if(msg.startsWith('SESSION:')) {
154
+				let session_id = msg.substring(8, msg.length)
155
+				let expire = new Date();
156
+
157
+				expire.setTime(expire.getTime()+(3600*24*1000))
158
+
159
+				let cookie = 'SESSION='+session_id
160
+				cookie += '; expires='+expire.toUTCString()
161
+				cookie += '; path=/';
162
+				cookie += '; SameSite=strict';
163
+				document.cookie = cookie
164
+
165
+				if(session_id != this.state.session_id) {
166
+					console.log('New session')
167
+					let tzname = Intl.DateTimeFormat().resolvedOptions().timeZone;
168
+					this.state.ws.send('alarm add alrm1')
169
+					this.state.ws.send('tzset '+util.escape_arg(tzname))
170
+				} else {
171
+					console.log('Restoring session '+session_id)
172
+				}
173
+				this.alrmUpdate()
174
+
175
+			} else if(msg.startsWith('OK:')) {
176
+				msg = msg.substring(3, msg.length)
177
+				if(msg.length > 0) {
178
+					let alarms = JSON.parse(msg)
179
+					this.setState({alarms:alarms})
180
+					for(let name in alarms) {
181
+						if(alarms[name].ringing) {
182
+							Notif.set(name,
183
+								alarms[name].time)
184
+						} else {
185
+							Notif.unset(name)
186
+						}
187
+					}
188
+				}
189
+			}
190
+			else if(msg.startsWith('ERR:')) {
191
+				this.setState({err:msg})
192
+			} else if (msg.startsWith('TZN:')) {
193
+				msg = msg.substring(4, msg.length)
194
+				this.setState({tzname:msg})
195
+			} else if (msg.startsWith('ALRM:')) {
196
+				msg = msg.substring(5, msg.length)
197
+				this.alrmUpdate()
198
+			} else {
199
+				this.setState({time:msg})
200
+			}
201
+		}
202
+
203
+		/* Websocket onclose handler : attempt to reconnect after 2s */
204
+		ws.onclose = (evt) => {
205
+			this.setState({	err: 'Connection lost, reconnecting...',
206
+					ws:null})
207
+			setTimeout( () => {
208
+				this.wsConnect()
209
+			}, 2000)
210
+		}
211
+
212
+		this.setState({ws:ws})
213
+	}
214
+}
215
+
216
+export default Clock;

+ 27
- 0
src/ClockDisp.css View File

@@ -0,0 +1,27 @@
1
+.ClockDisp {
2
+	text-align: center;
3
+}
4
+
5
+.ClockTime {
6
+	font-size: 5em;
7
+	margin-bottom: 0px;
8
+}
9
+
10
+.ClockNumber {
11
+	font-weight: normal;
12
+}
13
+
14
+.ClockTimeSep {
15
+	vertical-align: .1em;
16
+}
17
+
18
+.ClockDate {
19
+	font-size: 3em;
20
+}
21
+
22
+.ClockTz {
23
+	font-size: 1.5em;
24
+	font-style: italic;
25
+}
26
+
27
+

+ 56
- 0
src/ClockDisp.js View File

@@ -0,0 +1,56 @@
1
+import * as React from 'react';
2
+
3
+import './ClockDisp.css';
4
+
5
+class ClockDisp extends React.Component {
6
+	
7
+	constructor(props:{time:'', tzname:''}) {
8
+		super(props)
9
+		
10
+	}
11
+
12
+	render() {
13
+		let date, time, offset;
14
+		let year, month, day, hour, minute, second;
15
+
16
+		date = this.props.time.substring(0,10)
17
+		time = this.props.time.substring(11,19)
18
+		offset = this.props.time.substring(19,this.props.time.length)
19
+
20
+		year = date.substring(0,4)
21
+		month = date.substring(5,7)
22
+		day = date.substring(8,10)
23
+		hour = time.substring(0,2)
24
+		minute = time.substring(3,5)
25
+		second = time.substring(6,8)
26
+
27
+		document.title = time+offset
28
+		return (<div className="ClockDisp">
29
+				<p
30
+					className="ClockTime"
31
+				>
32
+					<span
33
+						className="ClockNumber ClockHour"
34
+					>{hour}</span>
35
+					<span
36
+						className="ClockTimeSep"
37
+					>:</span>
38
+					<span
39
+						className="ClockNumber ClockMinute"
40
+					>{minute}</span>
41
+					<span
42
+						className="ClockTimeSep"
43
+					>:</span>
44
+					<span
45
+						className="ClockNumber ClockSecond"
46
+					>{second}</span>
47
+				</p>
48
+				<p className="ClockDate">{date}</p>
49
+				<p className="ClockTz">{this.props.tzname} {offset}</p>
50
+			</div>
51
+		);
52
+	}
53
+
54
+}
55
+
56
+export default ClockDisp;

+ 1
- 0
src/config.json View File

@@ -0,0 +1 @@
1
+{"WS_URL":"ws://192.168.122.196/wsclock/"}

+ 10
- 0
src/index.css View File

@@ -0,0 +1,10 @@
1
+#root {
2
+}
3
+
4
+button {
5
+	border-radius: 10px;
6
+	background-color: #DDD;
7
+	color: #555;
8
+	border-style: solid;
9
+	border-color: #CCC;
10
+}

+ 13
- 0
src/index.js View File

@@ -0,0 +1,13 @@
1
+import * as React from 'react';
2
+import * as ReactDOM from 'react-dom';
3
+import Config from './config.json';
4
+import Clock from './Clock';
5
+import './index.css'
6
+
7
+ReactDOM.render(
8
+  <React.StrictMode>
9
+    <Clock wsurl={Config.WS_URL}/>
10
+  </React.StrictMode>,
11
+  document.getElementById('root')
12
+);
13
+

+ 1
- 0
src/react-app-env.d.ts View File

@@ -0,0 +1 @@
1
+/// <reference types="react-scripts" />

+ 10
- 0
src/utils.js View File

@@ -0,0 +1,10 @@
1
+
2
+function escape_arg(arg) {
3
+	return '"'+arg.replace('"', '\\"')+'"'
4
+}
5
+
6
+const util = {
7
+	escape_arg:escape_arg,
8
+}
9
+
10
+export default util;

+ 26
- 0
tsconfig.json View File

@@ -0,0 +1,26 @@
1
+{
2
+  "compilerOptions": {
3
+    "target": "es5",
4
+    "lib": [
5
+      "dom",
6
+      "dom.iterable",
7
+      "esnext"
8
+    ],
9
+    "allowJs": true,
10
+    "skipLibCheck": true,
11
+    "esModuleInterop": true,
12
+    "allowSyntheticDefaultImports": true,
13
+    "strict": true,
14
+    "forceConsistentCasingInFileNames": true,
15
+    "noFallthroughCasesInSwitch": true,
16
+    "module": "esnext",
17
+    "moduleResolution": "node",
18
+    "resolveJsonModule": true,
19
+    "isolatedModules": true,
20
+    "noEmit": true,
21
+    "jsx": "react-jsx"
22
+  },
23
+  "include": [
24
+    "src"
25
+  ]
26
+}

Loading…
Cancel
Save