Compare commits
34 Commits
db751a1d9e
...
master
Author | SHA1 | Date | |
---|---|---|---|
a1dc088a23 | |||
d4a19060e8 | |||
fe9f7e81e9 | |||
6e07c2be85 | |||
03db7575fd | |||
df7470f147 | |||
17a3436017 | |||
4bdcd49e1f | |||
031c1ede21 | |||
08e5c2e3d2 | |||
2c80c4b279 | |||
66d688291c | |||
49430114fe | |||
cfd0056197 | |||
facd8d912f | |||
b743cbc9a5 | |||
ace77028c4 | |||
dd215a8acc | |||
90a03dc17c | |||
e1c42c90d5 | |||
cb8f6daccc | |||
822eff6c5d | |||
ce86f1e029 | |||
7ed58e94d8 | |||
446afb939a | |||
cae5d1c95a | |||
fde5018da8 | |||
fcc81cf7e5 | |||
459d334732 | |||
c5a475f9c5 | |||
40e3cc9d7e | |||
90ba2410b9 | |||
3b0a212ad4 | |||
a4a55afa70 |
45
.eslintrc.js
Normal file
45
.eslintrc.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"extends": ["eslint:recommended", "plugin:react/recommended"],
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
"tab"
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"double"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"no-unused-vars": [1]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
src/config.js
|
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "osd-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.1.0",
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"grommet": "^2.16.2",
|
||||||
|
"grommet-icons": "^4.5.0",
|
||||||
|
"polished": "^4.0.5",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-redux": "^7.1.3",
|
||||||
|
"react-scripts": "4.0.1",
|
||||||
|
"socket.io-client": "^3.0.4",
|
||||||
|
"styled-components": "^5.2.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
43
public/index.html
Normal file
43
public/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React Redux App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
16
src/App.js
Normal file
16
src/App.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Grommet } from "grommet";
|
||||||
|
import Core from "./Core";
|
||||||
|
import WebSocketProvider from "./WebSocket";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<WebSocketProvider>
|
||||||
|
<Grommet plain>
|
||||||
|
<Core />
|
||||||
|
</Grommet>
|
||||||
|
</WebSocketProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
53
src/Core.js
Normal file
53
src/Core.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import { Box, Button, Clock, Text } from "grommet";
|
||||||
|
|
||||||
|
import { setPlugin, selectCore } from "./coreSlice";
|
||||||
|
import { selectVoice } from "./voiceSlice";
|
||||||
|
import VoiceBars from "./components/VoiceBars";
|
||||||
|
import plugins from "./plugins";
|
||||||
|
|
||||||
|
function Core() {
|
||||||
|
const coreState = useSelector(selectCore);
|
||||||
|
const voiceState = useSelector(selectVoice);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const plugin = plugins[coreState.plugin];
|
||||||
|
const props = {
|
||||||
|
data: coreState.data,
|
||||||
|
close: () => dispatch(setPlugin(false))
|
||||||
|
};
|
||||||
|
return <>
|
||||||
|
<Box height="5vh" background="light-1" justify="center" direction="row">
|
||||||
|
{voiceState.recording ? <VoiceBars style={{height: "100%"}}/>:
|
||||||
|
voiceState.text === null ? null:
|
||||||
|
<Text style={{paddingTop: "1em"}}>{voiceState.text}</Text>
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
{plugin ? React.createElement(plugin, props, null) : (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
>
|
||||||
|
<Clock size="xxlarge" />
|
||||||
|
<Clock size="xxlarge" type="digital" />
|
||||||
|
<Box direction="row" margin="1.5em" wrap={true}>
|
||||||
|
<Button onClick={() => dispatch(setPlugin("petMode"))} primary
|
||||||
|
label="Pet Mode" />
|
||||||
|
<Button onClick={() => dispatch(setPlugin("smartHome"))} primary
|
||||||
|
label="Smart Home" margin="0 0 0 1em" />
|
||||||
|
<Button onClick={() => dispatch(setPlugin("maps"))} primary
|
||||||
|
label="Maps" margin="0 0 0 1em" />
|
||||||
|
<Button onClick={() => dispatch(setPlugin("marketplace"))} primary
|
||||||
|
label="Marketplace" margin="0 0 0 1em" />
|
||||||
|
</Box>
|
||||||
|
</Box>)
|
||||||
|
}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Core;
|
57
src/WebSocket.js
Normal file
57
src/WebSocket.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { createContext } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import io from "socket.io-client";
|
||||||
|
import { WS_BASE } from "./config";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setData, setPlugin } from "./coreSlice";
|
||||||
|
import { setRecording, setText } from "./voiceSlice";
|
||||||
|
|
||||||
|
const WebSocketContext = createContext(null);
|
||||||
|
|
||||||
|
export { WebSocketContext };
|
||||||
|
|
||||||
|
function WebSocketProvider({ children }) {
|
||||||
|
let socket;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
if (!socket) {
|
||||||
|
socket = io.connect(WS_BASE);
|
||||||
|
|
||||||
|
socket.on("switchPlugin", (payload) => {
|
||||||
|
dispatch(setData(payload.data || {}));
|
||||||
|
dispatch(setPlugin(payload.plugin));
|
||||||
|
if (payload.time) {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(setData({}));
|
||||||
|
dispatch(setPlugin(false));
|
||||||
|
}, payload.time);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("voice:wakeword", () => {
|
||||||
|
dispatch(setRecording(true));
|
||||||
|
dispatch(setText(null));
|
||||||
|
});
|
||||||
|
socket.on("voice:record_end", () => {
|
||||||
|
dispatch(setRecording(false));
|
||||||
|
dispatch(setText(null));
|
||||||
|
});
|
||||||
|
socket.on("voice:utterance", payload => {
|
||||||
|
dispatch(setRecording(false));
|
||||||
|
dispatch(setText(payload.text));
|
||||||
|
setTimeout(() => dispatch(setText(null)), 3000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebSocketContext.Provider value={socket}>
|
||||||
|
{children}
|
||||||
|
</WebSocketContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSocketProvider.propTypes = {
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebSocketProvider;
|
10
src/app/store.js
Normal file
10
src/app/store.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import coreReducer from "../coreSlice";
|
||||||
|
import voiceReducer from "../voiceSlice";
|
||||||
|
|
||||||
|
export default configureStore({
|
||||||
|
reducer: {
|
||||||
|
core: coreReducer,
|
||||||
|
voice: voiceReducer,
|
||||||
|
},
|
||||||
|
});
|
79
src/components/VoiceBars.js
Normal file
79
src/components/VoiceBars.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function VoiceBars() {
|
||||||
|
return (
|
||||||
|
<div className="waves" style={{transform: "rotate(180deg)", marginTop: "130px", transition: "1s"}}>
|
||||||
|
<svg width="100vw" fill="none" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#00B4DB" />
|
||||||
|
<stop offset="50%" stopColor="#224488" />
|
||||||
|
<stop offset="100%" stopColor="#0083B0" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
fill="url(#grad1)"
|
||||||
|
d="
|
||||||
|
M0 67
|
||||||
|
C 273,183
|
||||||
|
822,-40
|
||||||
|
1920.00,106
|
||||||
|
|
||||||
|
V 359
|
||||||
|
H 0
|
||||||
|
V 67
|
||||||
|
Z">
|
||||||
|
<animate
|
||||||
|
repeatCount="indefinite"
|
||||||
|
fill="url(#grad1)"
|
||||||
|
attributeName="d"
|
||||||
|
dur="5s"
|
||||||
|
attributeType="XML"
|
||||||
|
values="
|
||||||
|
M0 77
|
||||||
|
C 473,283
|
||||||
|
822,-40
|
||||||
|
1920,116
|
||||||
|
|
||||||
|
V 359
|
||||||
|
H 0
|
||||||
|
V 67
|
||||||
|
Z;
|
||||||
|
|
||||||
|
M0 77
|
||||||
|
C 473,-40
|
||||||
|
1222,283
|
||||||
|
1920,136
|
||||||
|
|
||||||
|
V 359
|
||||||
|
H 0
|
||||||
|
V 67
|
||||||
|
Z;
|
||||||
|
|
||||||
|
M0 77
|
||||||
|
C 973,260
|
||||||
|
1722,-53
|
||||||
|
1920,120
|
||||||
|
|
||||||
|
V 359
|
||||||
|
H 0
|
||||||
|
V 67
|
||||||
|
Z;
|
||||||
|
|
||||||
|
M0 77
|
||||||
|
C 473,283
|
||||||
|
822,-40
|
||||||
|
1920,116
|
||||||
|
|
||||||
|
V 359
|
||||||
|
H 0
|
||||||
|
V 67
|
||||||
|
Z
|
||||||
|
">
|
||||||
|
</animate>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VoiceBars;
|
17
src/coreSlice.js
Normal file
17
src/coreSlice.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export const coreSlice = createSlice({
|
||||||
|
name: "core",
|
||||||
|
initialState: {
|
||||||
|
plugin: false,
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setPlugin: (state, action) => ({...state, plugin: action.payload}),
|
||||||
|
setData: (state, action) => ({...state, data: action.payload}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setPlugin, setData } = coreSlice.actions;
|
||||||
|
export const selectCore = state => state.core;
|
||||||
|
export default coreSlice.reducer;
|
13
src/index.css
Normal file
13
src/index.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
21
src/index.js
Normal file
21
src/index.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
import store from "./app/store";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import * as serviceWorker from "./serviceWorker";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
85
src/plugins/Accident.js
Normal file
85
src/plugins/Accident.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Box, Button, Heading, Text } from "grommet";
|
||||||
|
import { Aid, Impact } from "grommet-icons";
|
||||||
|
|
||||||
|
import Telegram from "../utils/telegram";
|
||||||
|
import { TG_API, TG_USERID } from "../config";
|
||||||
|
|
||||||
|
function sendNotification(close) {
|
||||||
|
if (window.sendingAccidentAlert)
|
||||||
|
return;
|
||||||
|
window.sendingAccidentAlert = true;
|
||||||
|
const bot = new Telegram(TG_API);
|
||||||
|
bot.sendMessage(TG_USERID, "User detected in an accident. Location has been attached below.");
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
position => {
|
||||||
|
window.sendingAccidentAlert = false;
|
||||||
|
bot.sendLocation( TG_USERID,
|
||||||
|
position.coords.latitude, position.coords.longitude);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
window.sendingAccidentAlert = false;
|
||||||
|
bot.sendMessage(TG_USERID, "Error retrieving location");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setTimeout(close, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Accident(props) {
|
||||||
|
const threshold = 20;
|
||||||
|
const [ time, setTime ] = useState(threshold);
|
||||||
|
const countRef = useRef(null);
|
||||||
|
|
||||||
|
function decrementTime(time) {
|
||||||
|
if (time === 0) {
|
||||||
|
clearInterval(countRef.current);
|
||||||
|
sendNotification(props.close);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return time - 1;
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
countRef.current = setInterval(() => setTime(decrementTime), 1000);
|
||||||
|
return () => clearInterval(countRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
clearTimeout(countRef.current);
|
||||||
|
props.close();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
time === 0 ?
|
||||||
|
<Aid color="plain" size="xlarge" />
|
||||||
|
:<Impact color="status-critical" size="xlarge" />
|
||||||
|
}
|
||||||
|
<Heading>{
|
||||||
|
time === 0 ?
|
||||||
|
"Calling Ambulance!"
|
||||||
|
:"Accident Detected!"
|
||||||
|
}</Heading>
|
||||||
|
<Text>{
|
||||||
|
time === 0 ? "Calling Ambulance":
|
||||||
|
`To cancel calling the ambulance, press the button below. (${time})`
|
||||||
|
}</Text>
|
||||||
|
<Button primary size="large" onClick={dismiss}
|
||||||
|
label="Dismiss" margin="1.5em" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Accident.propTypes = {
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Accident;
|
35
src/plugins/GenericPageWithIcon.js
Normal file
35
src/plugins/GenericPageWithIcon.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Box, Button, Heading, Text } from "grommet";
|
||||||
|
|
||||||
|
function GenericPageWithIcon(props) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
style={{
|
||||||
|
padding: "0 3em",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.icon}
|
||||||
|
<Heading>{props.title}</Heading>
|
||||||
|
<Text style={{ textAlign: "center" }}>{props.description}</Text>
|
||||||
|
<Button primary size="large" onClick={props.close}
|
||||||
|
label="Dismiss" margin="1.5em" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GenericPageWithIcon.propTypes = {
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
icon: PropTypes.node,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenericPageWithIcon;
|
23
src/plugins/Manual.js
Normal file
23
src/plugins/Manual.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import GenericPageWithIcon from "./GenericPageWithIcon";
|
||||||
|
import { StatusInfo } from "grommet-icons";
|
||||||
|
|
||||||
|
function Manual(props) {
|
||||||
|
return (
|
||||||
|
<GenericPageWithIcon
|
||||||
|
title={props.data.title}
|
||||||
|
description={props.data.description}
|
||||||
|
icon={<StatusInfo color="plain" size="xlarge" />}
|
||||||
|
close={props.close}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Manual.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
Manual.pluginName = "Manual";
|
||||||
|
|
||||||
|
export default Manual;
|
53
src/plugins/Maps.js
Normal file
53
src/plugins/Maps.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Box, Button } from "grommet";
|
||||||
|
|
||||||
|
import { MAPS_API } from "../config";
|
||||||
|
|
||||||
|
function Maps(props) {
|
||||||
|
const [ location, setLocation ] = useState([null, null]);
|
||||||
|
useEffect(() => {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
position => setLocation([position.coords.latitude, position.coords.longitude]),
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapsEmbedUrl = new URL("https://www.google.com/maps/embed/v1/view");
|
||||||
|
mapsEmbedUrl.searchParams.append("key", MAPS_API);
|
||||||
|
mapsEmbedUrl.searchParams.append("center", `${location[0]},${location[1]}`);
|
||||||
|
mapsEmbedUrl.searchParams.append("zoom", 14);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
>
|
||||||
|
{location[0] === null ?
|
||||||
|
"Loading Location":
|
||||||
|
<iframe
|
||||||
|
width="650"
|
||||||
|
height="300"
|
||||||
|
frameBorder="0"
|
||||||
|
style={{ border: 0 }}
|
||||||
|
src={mapsEmbedUrl.href}>
|
||||||
|
</iframe>
|
||||||
|
}
|
||||||
|
<Button primary size="large" onClick={props.close}
|
||||||
|
label="Dismiss" margin="1.5em" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Maps.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
Maps.pluginName = "Maps";
|
||||||
|
|
||||||
|
export default Maps;
|
67
src/plugins/Marketplace.js
Normal file
67
src/plugins/Marketplace.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Box, Button, Card, CardBody, CardFooter, CardHeader, Heading } from "grommet";
|
||||||
|
import { InstallOption, Trash } from "grommet-icons";
|
||||||
|
|
||||||
|
|
||||||
|
function Marketplace(props) {
|
||||||
|
const plugins = [
|
||||||
|
{
|
||||||
|
name: "Maps",
|
||||||
|
description: "Google Maps is a mapping service. It offers satellite imagery, aerial photography, street maps, 360° interactive panoramic views of streets, real-time traffic conditions, and route planning for traveling.",
|
||||||
|
installed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Smart Home",
|
||||||
|
description: "Smart Home Plugin lets you integrate with various well known smart devices allowing you to stream video from various devices.",
|
||||||
|
installed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Spotify",
|
||||||
|
description: "With Spotify, you have access to a world of music. You can listen to artists and albums, or create your own playlist of your favourite songs.",
|
||||||
|
installed: false
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
>
|
||||||
|
<Box wrap="wrap" direction="row">
|
||||||
|
{
|
||||||
|
plugins.map(i =>
|
||||||
|
<Card key={i.name} style={{margin: "1em", width: "30%"}}>
|
||||||
|
<CardHeader pad="medium"><Heading>{i.name}</Heading></CardHeader>
|
||||||
|
<CardBody pad="medium">{i.description}</CardBody>
|
||||||
|
<CardFooter pad="medium" justify="center">
|
||||||
|
<Button
|
||||||
|
label={i.installed ? "Uninstall": "Install"}
|
||||||
|
icon={i.installed ?
|
||||||
|
<Trash color="status-error" /> :
|
||||||
|
<InstallOption color="status-ok" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
<Button primary size="large" onClick={props.close}
|
||||||
|
label="Dismiss" margin="1.5em" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Marketplace.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
Marketplace.pluginName = "Marketplace";
|
||||||
|
|
||||||
|
export default Marketplace;
|
59
src/plugins/PetMode.js
Normal file
59
src/plugins/PetMode.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import GenericPageWithIcon from "./GenericPageWithIcon";
|
||||||
|
import { Sun } from "grommet-icons";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import Telegram from "../utils/telegram";
|
||||||
|
import { CAR_API, TG_API, TG_USERID } from "../config";
|
||||||
|
|
||||||
|
function sendNotification(temp) {
|
||||||
|
if (window.sendingPetAlert)
|
||||||
|
return;
|
||||||
|
window.sendingPetAlert = true;
|
||||||
|
const bot = new Telegram(TG_API);
|
||||||
|
bot.sendMessage(TG_USERID, `The temperature in the car is ${temp}°C. Please check your child/pet.`);
|
||||||
|
window.sendingPetAlert = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PetMode(props) {
|
||||||
|
const [ temp, setTemp ] = useState(null);
|
||||||
|
const [ , setNotified ] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get(`${CAR_API}data/InsideTemperature`)
|
||||||
|
.then(resp => setTemp(resp.data.value));
|
||||||
|
const id = setInterval(
|
||||||
|
() => axios.get(`${CAR_API}data/InsideTemperature`)
|
||||||
|
.then(resp => {
|
||||||
|
setTemp(resp.data.value);
|
||||||
|
if (resp.data.value > 35 || resp.data.value < 5) {
|
||||||
|
setNotified(notified => {
|
||||||
|
if (notified)
|
||||||
|
return true;
|
||||||
|
sendNotification(resp.data.value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
if (temp === null)
|
||||||
|
return <></>;
|
||||||
|
return (
|
||||||
|
<GenericPageWithIcon
|
||||||
|
title={temp === null ? "Fetching temperature": `${temp}°C`}
|
||||||
|
description={`The temperature inside is ${temp}°C. The pet / child is ${temp > 35 ? "un": ""}safe.`}
|
||||||
|
icon={<Sun color={temp > 35 ? "status-critical": "plain"} size="xlarge" />}
|
||||||
|
close={props.close}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
PetMode.propTypes = {
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
PetMode.pluginName = "Temperature";
|
||||||
|
|
||||||
|
export default PetMode;
|
50
src/plugins/SmartHome.js
Normal file
50
src/plugins/SmartHome.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React, {useState, useEffect} from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Box, Button } from "grommet";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import { CAMERA_URL, LIGHTS_URL } from "../config";
|
||||||
|
|
||||||
|
function SmartHome(props) {
|
||||||
|
const [ idx, setIdx ] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setIdx(i => i + 1), 20);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
function turnOn() {
|
||||||
|
axios.get(`${LIGHTS_URL}/on`);
|
||||||
|
}
|
||||||
|
function turnOff() {
|
||||||
|
axios.get(`${LIGHTS_URL}/off`);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
align="center"
|
||||||
|
background="light-1"
|
||||||
|
width="100vw"
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
alignContent="center"
|
||||||
|
height="95vh"
|
||||||
|
>
|
||||||
|
<img src={`${CAMERA_URL}?id=${idx}`} />
|
||||||
|
<Box direction="row" margin="1em 0">
|
||||||
|
<Button primary size="large" onClick={turnOn}
|
||||||
|
label="Turn On Lights" margin="0 0 0 0" />
|
||||||
|
<Button primary size="large" onClick={turnOff}
|
||||||
|
label="Turn Off Lights" margin="0 0 0 1em" />
|
||||||
|
</Box>
|
||||||
|
<Button primary size="large" onClick={props.close}
|
||||||
|
label="Dismiss" margin="0" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SmartHome.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
SmartHome.pluginName = "Smart Home";
|
||||||
|
|
||||||
|
export default SmartHome;
|
23
src/plugins/Temperature.js
Normal file
23
src/plugins/Temperature.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import GenericPageWithIcon from "./GenericPageWithIcon";
|
||||||
|
import { Sun } from "grommet-icons";
|
||||||
|
|
||||||
|
function Temperature(props) {
|
||||||
|
return (
|
||||||
|
<GenericPageWithIcon
|
||||||
|
title={props.data.temperature}
|
||||||
|
description={`The current temperature outside is ${props.data.temperature}℃`}
|
||||||
|
icon={<Sun color="plain" size="xlarge" />}
|
||||||
|
close={props.close}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Temperature.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
Temperature.pluginName = "Temperature";
|
||||||
|
|
||||||
|
export default Temperature;
|
23
src/plugins/Warning.js
Normal file
23
src/plugins/Warning.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import GenericPageWithIcon from "./GenericPageWithIcon";
|
||||||
|
import { Alert } from "grommet-icons";
|
||||||
|
|
||||||
|
function Warning(props) {
|
||||||
|
return (
|
||||||
|
<GenericPageWithIcon
|
||||||
|
title={props.data.title}
|
||||||
|
description={props.data.description}
|
||||||
|
icon={<Alert color="status-critical" size="xlarge" />}
|
||||||
|
close={props.close}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
Warning.propTypes = {
|
||||||
|
data: PropTypes.object,
|
||||||
|
close: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
Warning.pluginName = "Warning";
|
||||||
|
|
||||||
|
export default Warning;
|
19
src/plugins/index.js
Normal file
19
src/plugins/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Warning from "./Warning";
|
||||||
|
import Manual from "./Manual";
|
||||||
|
import Temperature from "./Temperature";
|
||||||
|
import Accident from "./Accident";
|
||||||
|
import PetMode from "./PetMode";
|
||||||
|
import SmartHome from "./SmartHome";
|
||||||
|
import Maps from "./Maps";
|
||||||
|
import Marketplace from "./Marketplace";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
warning: Warning,
|
||||||
|
manual: Manual,
|
||||||
|
temperature: Temperature,
|
||||||
|
accident: Accident,
|
||||||
|
petMode: PetMode,
|
||||||
|
smartHome: SmartHome,
|
||||||
|
maps: Maps,
|
||||||
|
marketplace: Marketplace,
|
||||||
|
};
|
9
src/sample.config.js
Normal file
9
src/sample.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const WS_BASE = "http://localhost:5050/";
|
||||||
|
const TG_API = "xxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx";
|
||||||
|
const TG_USERID = 123456789;
|
||||||
|
const CAR_API = "http://localhost:5000/";
|
||||||
|
const CAMERA_URL = "http://path.to/still/image.jpg";
|
||||||
|
const MAPS_API = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
|
||||||
|
const LIGHTS_URL = "http://192.168.1.50:5000/";
|
||||||
|
|
||||||
|
export { WS_BASE, TG_API, TG_USERID, CAR_API, CAMERA_URL, MAPS_API, LIGHTS_URL };
|
137
src/serviceWorker.js
Normal file
137
src/serviceWorker.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === "localhost" ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === "[::1]" ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won"t work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let"s check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
"This web app is being served cache-first by a service " +
|
||||||
|
"worker. To learn more, visit https://bit.ly/CRA-PWA"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === "installed") {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
"New content is available and will be used when all " +
|
||||||
|
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It"s the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log("Content is cached for offline use.");
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error during service worker registration:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can"t reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { "Service-Worker": "script" },
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf("javascript") === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
"No internet connection found. App is running in offline mode."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
0
src/utils/index.js
Normal file
0
src/utils/index.js
Normal file
19
src/utils/telegram.js
Normal file
19
src/utils/telegram.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
class Telegram {
|
||||||
|
constructor(token) {
|
||||||
|
this.axios = axios.create({
|
||||||
|
baseURL: `https://api.telegram.org/bot${token}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(chat_id, text) {
|
||||||
|
this.axios.post("/sendMessage", {chat_id, text});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendLocation(chat_id, latitude, longitude) {
|
||||||
|
this.axios.post("/sendLocation", {chat_id, latitude, longitude});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Telegram;
|
17
src/voiceSlice.js
Normal file
17
src/voiceSlice.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export const voiceSlice = createSlice({
|
||||||
|
name: "voice",
|
||||||
|
initialState: {
|
||||||
|
recording: false,
|
||||||
|
text: null
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setRecording: (state, action) => ({...state, recording: action.payload}),
|
||||||
|
setText: (state, action) => ({...state, text: action.payload}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setRecording, setText } = voiceSlice.actions;
|
||||||
|
export const selectVoice = state => state.voice;
|
||||||
|
export default voiceSlice.reducer;
|
Reference in New Issue
Block a user