First commit
This commit is contained in:
commit
9d69521b26
14 changed files with 3636 additions and 0 deletions
5
README
Normal file
5
README
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# RZL Map
|
||||
|
||||
Die Config ist in src/config.js erreichbar.
|
||||
|
||||
Doku folgt. Bald™
|
||||
27
index.html
Normal file
27
index.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.2.0/dist/leaflet.css" integrity="sha512-M2wvCLH6DSRazYeZRIm1JnYyh22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT/fJISVA1r/zQ==" crossorigin=""/>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
#content {
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.leaflet-container {
|
||||
height: calc(100vh - 64px);
|
||||
max-height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#drawer_uiComponents {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
|
||||
</div>
|
||||
<script src="public/dist/main.js"></script>
|
||||
</body>
|
||||
32
package.json
Normal file
32
package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "reacttest",
|
||||
"version": "1.0.0",
|
||||
"author": "uwap <me@uwap.name>",
|
||||
"description": "react",
|
||||
"scripts": {
|
||||
"build": "webpack --bail"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.2.0",
|
||||
"lodash": "^4.17.4",
|
||||
"material-ui": "^0.18.7",
|
||||
"mqtt": "^2.11.0",
|
||||
"ramda": "^0.24.1",
|
||||
"react": "^15.6.1",
|
||||
"react-dom": "^15.6.1",
|
||||
"react-leaflet": "^1.5.0",
|
||||
"react-tap-event-plugin": "^2.0.1",
|
||||
"redux": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.24.1",
|
||||
"babel-core": "^6.25.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"flow": "^0.2.3",
|
||||
"flow-bin": "^0.50.0",
|
||||
"webpack": "^3.1.0"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
2
rzl.svg
Normal file
2
rzl.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="571px" height="341px" version="1.1" content="<mxfile userAgent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36" version="7.1.6" editor="www.draw.io"><diagram id="99136bbf-b1d7-fc77-4446-920ab02a96c1" name="Page-1">3ZdNj5swEIZ/jY8rYRsIHBeatlK1p1Tq2YsdsGJw5DgL6a+vCSZ8RuLASnRzieed8dczYzAAx3n1Q5Fz9iYpEwA5tAL4G0AIutA1f7Vya5RdbdVCqji1QZ1w4H+ZFR2rXjlll0GgllJofh6KiSwKluiBRpSS5TDsKMVw1jNJ2UQ4JERM1T+c6qxRA7Tr9J+Mp1k7M/TDxvNOklOq5LWw8wGEj/df485JO5bd6CUjVJY9Ce8BjpWUumnlVcxEzbbF1vT7/sT7WLdihV7Uweblg4gra5d8X5i+tTDu22F1BwfgqMy4ZoczSWpvadJvtEznwljQNO1wTGlWPV0TfOzUVBCTOdPqZkKqYRG0tWPNskuE12pZLwnYtSKxyU8fI3cATMMyeMID/488kDPDAwZr8PC2x2MM5HGYekQgnKuQNYD42wcyUyEw/Cweu+3zgM4UiDt3YtAaQILtAcH+CAheCASuASTcHhA3HAIJF/JY4xXTrnZTPIY48NyBCWaA+GvwcCc8fgPkCzNFRPmHaaZ1k7SaGa8nz0SqxZF0cSRfHHl5FjlKsUmWHubxopU8sVgKqYxSyMJERkcuxEgigqeFMROTXGb0qE49N5fXV+vIOaX1NLOF8wmnB3reouOzxuMVTS8kv0CMQRQnGftKkFEwhrzsGbUK5OklZ/+yT05fCvACvt46fI3ZfdHdfb3PZrz/Bw==</diagram></mxfile>" style="background-color: rgb(255, 255, 255);"><defs/><g transform="translate(0.5,0.5)"><rect x="0" y="0" width="570" height="340" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="0" y="0" width="200" height="180" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="150" width="110" height="30" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="0" width="190" height="30" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="200" y="30" width="40" height="120" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="350" y="60" width="40" height="110" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="480" y="20" width="40" height="140" fill="#ffffff" stroke="#000000" pointer-events="none"/><rect x="30" y="230" width="480" height="60" fill="#ffffff" stroke="#000000" pointer-events="none"/><g transform="translate(495.5,53.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="8" height="82" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 8px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">T<div>a</div><div>r</div><div>d</div><div>i</div><div>s</div></div></div></foreignObject><text x="4" y="47" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">[Not supported by viewer]</text></switch></g><g transform="translate(272.5,83.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="34" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 36px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">Küche</div></div></foreignObject><text x="17" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">Küche</text></switch></g><g transform="translate(75.5,83.5)"><switch><foreignObject style="overflow:visible;" pointer-events="all" width="38" height="12" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 40px; white-space: nowrap; word-wrap: normal; text-align: center;"><div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;">E-Ecke</div></div></foreignObject><text x="19" y="12" fill="#000000" text-anchor="middle" font-size="12px" font-family="Helvetica">E-Ecke</text></switch></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
20
src/UiItems.js
Normal file
20
src/UiItems.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import Toggle from "material-ui/Toggle";
|
||||
import Config from "./config";
|
||||
import R from "ramda";
|
||||
|
||||
const onToggle = (topic, props, state) => (x, toggled) =>
|
||||
state.mqtt.publish(Config.topics[topic].command,
|
||||
toggled ? Config.topics[topic].values[R.propOr("on", "on", props)]
|
||||
: Config.topics[topic].values[R.propOr("off", "off", props)]);
|
||||
|
||||
export const toggle = (props: Object) => (
|
||||
<Toggle label={props.text}
|
||||
toggled={
|
||||
props.state.values[props.topic] ===
|
||||
Config.topics[props.topic].values[R.propOr("on", "on", props)]
|
||||
}
|
||||
onToggle={onToggle(props.topic, props, props.state)}
|
||||
/>
|
||||
);
|
||||
37
src/appbar.js
Normal file
37
src/appbar.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import AppBar from "material-ui/AppBar";
|
||||
import CircularProgress from "material-ui/CircularProgress";
|
||||
import DoneIcon from "material-ui/svg-icons/action/done";
|
||||
import IconMenu from "material-ui/IconMenu";
|
||||
import IconButton from "material-ui/IconButton";
|
||||
import MenuItem from "material-ui/MenuItem";
|
||||
import { orange400, grey50 } from "material-ui/styles/colors";
|
||||
|
||||
const TopBarIndicatorMenu = (props: Object) => (
|
||||
<IconMenu
|
||||
iconButtonElement={
|
||||
<IconButton style={{width:48, height:48, padding: 0}}
|
||||
iconStyle={{width:48, height: 48}}
|
||||
tooltip="Connected!">
|
||||
<DoneIcon color={grey50} />
|
||||
</IconButton>}
|
||||
style={{width:48, height:48}}>
|
||||
<MenuItem primaryText="Reconnect" />
|
||||
</IconMenu>
|
||||
);
|
||||
|
||||
|
||||
const TopBarIndicator = (props: Object) => (
|
||||
props.mqtt == null ? <CircularProgress size={48} color={grey50} />
|
||||
: <TopBarIndicatorMenu {...props} />
|
||||
);
|
||||
|
||||
const TopBar = (props: Object) => (
|
||||
<AppBar title={props.title}
|
||||
style={{background:orange400}}
|
||||
iconElementLeft={<TopBarIndicator {...props} />}
|
||||
className="nav"
|
||||
/>);
|
||||
|
||||
export default TopBar;
|
||||
126
src/config.js
Normal file
126
src/config.js
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// @flow
|
||||
const config : Config = {
|
||||
topics: {
|
||||
led_stahltraeger: {
|
||||
state: "/service/openhab/out/pca301_ledstrips/state",
|
||||
command: "/service/openhab/in/pca301_ledstrips/command",
|
||||
value: "OFF", # defaultValue
|
||||
values: { on: "ON", off: "OFF" }
|
||||
},
|
||||
snackbar: {
|
||||
state: "/service/openhab/out/pca301_snackbar/state",
|
||||
command: "/service/openhab/in/pca301_snackbar/command",
|
||||
value: "OFF",
|
||||
values: { on: "ON", off: "OFF" }
|
||||
},
|
||||
twinkle: {
|
||||
state: "/service/openhab/out/pca301_twinkle/state",
|
||||
command: "/service/openhab/in/pca301_twinkle/command",
|
||||
value: "OFF",
|
||||
values: { on: "ON", off: "OFF" }
|
||||
},
|
||||
flyfry: {
|
||||
state: "/service/openhab/out/wifi_flyfry/state",
|
||||
command: "/service/openhab/in/wifi_flyfry/command",
|
||||
value: "OFF",
|
||||
values: { on: "ON", off: "OFF" }
|
||||
},
|
||||
artnet: {
|
||||
state: "/artnet/state",
|
||||
command: "/artnet/push",
|
||||
value: "blackout",
|
||||
values: { off: "blackout", yellow: "yellow", purple: "purple",
|
||||
blue: "blue", green: "green", red: "red", random: "random",
|
||||
cycle: "cycle-random" }
|
||||
}
|
||||
},
|
||||
controls: {
|
||||
led_stahltrager: {
|
||||
name: "LED Stahlträger",
|
||||
position: [360, 80],
|
||||
icon: "",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Stahlträger LED",
|
||||
topic: "led_stahltraeger"
|
||||
},
|
||||
]
|
||||
},
|
||||
snackbar: {
|
||||
name: "Snackbar",
|
||||
position: [550, 200],
|
||||
icon: "",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Snackbar",
|
||||
topic: "snackbar"
|
||||
}
|
||||
]
|
||||
},
|
||||
twinkle: {
|
||||
name: "Twinkle",
|
||||
position: [500, 280],
|
||||
icon: "",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Twinkle",
|
||||
topic: "twinkle"
|
||||
}
|
||||
]
|
||||
},
|
||||
flyfry: {
|
||||
name: "Fliegenbratgerät",
|
||||
position: [450, 320],
|
||||
icon: "",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Fliegenbratgerät",
|
||||
topic: "flyfry"
|
||||
}
|
||||
]
|
||||
},
|
||||
artnet: {
|
||||
name: "Artnet",
|
||||
position: [550,150],
|
||||
icon: "",
|
||||
ui: [
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Gelb",
|
||||
topic: "artnet",
|
||||
on: "yellow"
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Rot",
|
||||
topic: "artnet",
|
||||
on: "red"
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Pink",
|
||||
topic: "artnet",
|
||||
on: "purple"
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Grün",
|
||||
topic: "artnet",
|
||||
on: "green"
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
text: "Cycle Random",
|
||||
topic: "artnet",
|
||||
on: "cycle"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
92
src/index.jsx
Normal file
92
src/index.jsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
||||
import Drawer from 'material-ui/Drawer';
|
||||
import injectTapEventPlugin from 'react-tap-event-plugin';
|
||||
import { createStore } from "redux";
|
||||
import { MQTT_CONNECT } from "./stateActions";
|
||||
import connectMqtt from "./mqtt";
|
||||
import AppBar from "./appbar";
|
||||
import Toggle from "material-ui/Toggle";
|
||||
import * as UiItems from "./UiItems.js";
|
||||
import SpaceMap from "./map.js";
|
||||
import R from "ramda";
|
||||
import Config from "./config";
|
||||
import { Toolbar, ToolbarGroup, ToolbarTitle } from "material-ui/Toolbar";
|
||||
|
||||
injectTapEventPlugin();
|
||||
|
||||
const appState : State = {
|
||||
mqtt: null,
|
||||
ui: null,
|
||||
values: R.map(R.prop("value"), Config.topics)
|
||||
};
|
||||
|
||||
console.log(appState.values);
|
||||
|
||||
const handleEvent = (state = appState, action) => {
|
||||
switch (action.type) {
|
||||
case "CONNECT":
|
||||
return R.merge(state, { mqtt: action.mqtt });
|
||||
case "uiopen":
|
||||
return R.merge(state, { ui: action.ui });
|
||||
case "uiclose":
|
||||
return R.merge(state, { ui: null });
|
||||
case "mqtt_message":
|
||||
console.log(action.topic + ": " + action.message.toString());
|
||||
const val = (topic: string) =>
|
||||
Config.topics[topic].parseState == null ?
|
||||
action.message.toString() :
|
||||
Config.topics[topic].parseState(action.message);
|
||||
const keysToUpdate = R.keys(R.pickBy(val => val.state == action.topic,
|
||||
Config.topics));
|
||||
return R.mergeDeepRight(state, R.objOf("values", R.mergeAll(R.map(
|
||||
k => R.objOf(k, val(k)), keysToUpdate))));
|
||||
/*
|
||||
return R.merge(state, R.objOf("topics", R.merge(state.topics,
|
||||
R.map(R.merge(R.__, { value: val }),
|
||||
R.pickBy(val => val.state == action.topic, Config.topics)))));
|
||||
*/
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const store = createStore(handleEvent);
|
||||
|
||||
const UiItem = (state) => (props) =>
|
||||
UiItems[props.type](R.merge(props, {state:state}));
|
||||
|
||||
const renderUi = (state, key) => key != null && Config.controls[key] != null ?
|
||||
R.map(UiItem(state), Config.controls[key].ui) : null;
|
||||
|
||||
const App = (state: State) => (
|
||||
<div>
|
||||
<MuiThemeProvider>
|
||||
<div>
|
||||
<AppBar title="RZL Map" {...state} />
|
||||
<Drawer open={state.ui != null} openSecondary={true} disableSwipeToOpen={true}>
|
||||
<Toolbar>
|
||||
<ToolbarGroup firstChild={true}>
|
||||
<ToolbarTitle text={
|
||||
state.ui == null ? "" : Config.controls[state.ui].name}
|
||||
style={{"marginLeft": 10}} />
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
<div id="drawer_uiComponents">
|
||||
{renderUi(state, state.ui)}
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
</MuiThemeProvider>
|
||||
<SpaceMap width={640} height={400} image="rzl.svg" zoom={0.1}
|
||||
store={store} state={state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
store.subscribe(() => ReactDOM.render(<App {...store.getState()} />, document.getElementById("content")));
|
||||
|
||||
store.dispatch({type: null});
|
||||
store.dispatch({type: "mqtt_message", topic: "/service/openhab/out/pca301_ledstrips/state", message: "ON"});
|
||||
|
||||
connectMqtt("ws://172.22.36.207:1884", store); // wss://mqtt.starletp9.de/mqtt", store);
|
||||
29
src/map.js
Normal file
29
src/map.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import { Map, ImageOverlay, Marker, Popup } from "react-leaflet";
|
||||
import Leaflet from "leaflet";
|
||||
import R from "ramda";
|
||||
import Config from "./config";
|
||||
|
||||
// convert width/height coordinates to -height/width coordinates
|
||||
const c = (p) => [-p[1], p[0]]
|
||||
|
||||
const Markers = (props) => R.values(R.mapObjIndexed((el,key) => (
|
||||
<Marker position={c(el.position)} key={el.name}>
|
||||
<Popup onOpen={() => props.store.dispatch({type: "uiopen", ui: key})}
|
||||
onClose={() => props.store.dispatch({type: "uiclose"})}>
|
||||
<span>{el.name}</span>
|
||||
</Popup>
|
||||
</Marker>
|
||||
), R.propOr({}, "controls", Config)));
|
||||
|
||||
const SpaceMap = (props: Object) => (
|
||||
<Map center={c([props.width / 2, props.height / 2])} zoom={props.zoom}
|
||||
crs={Leaflet.CRS.Simple}>
|
||||
<ImageOverlay url={props.image}
|
||||
bounds={[c([0,0]), c([props.width, props.height])]} />
|
||||
{Markers(props)}
|
||||
</Map>
|
||||
);
|
||||
|
||||
export default SpaceMap;
|
||||
22
src/mqtt.js
Normal file
22
src/mqtt.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// @flow
|
||||
import mqtt from "mqtt";
|
||||
import { MQTT_MESSAGE, MQTT_CONNECT, MQTT_DISCONNECT } from "./stateActions";
|
||||
import { Store } from "redux";
|
||||
import Config from "./config";
|
||||
import R from "ramda";
|
||||
|
||||
export default function connectMqtt(url: string, store: Store<*,*>) {
|
||||
const client = mqtt.connect(url);
|
||||
client.on("connect", () => {
|
||||
store.dispatch({
|
||||
type: MQTT_CONNECT, mqtt: client
|
||||
});
|
||||
R.forEachObjIndexed(v =>
|
||||
client.subscribe(v.state), Config.topics);
|
||||
});
|
||||
client.on("message", (topic, message) => {
|
||||
store.dispatch({
|
||||
type: "mqtt_message", message: message, topic: topic
|
||||
});
|
||||
});
|
||||
}
|
||||
5
src/stateActions.js
Normal file
5
src/stateActions.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export const MQTT_DISCONNECT = "DISCONNECT";
|
||||
export const MQTT_CONNECT = "CONNECT";
|
||||
export const MQTT_MESSAGE = "MESSAGE";
|
||||
35
types/types.js
Normal file
35
types/types.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
declare type Map<K,V> = { [K]: V };
|
||||
|
||||
declare type Topic = {
|
||||
state: string,
|
||||
command: string,
|
||||
value: any,
|
||||
values: Map<string,any>,
|
||||
parseState?: (msg: Object) => any
|
||||
};
|
||||
declare type Topics = Map<string,Topic>;
|
||||
|
||||
declare type ControlUI = {
|
||||
type: "toggle",
|
||||
text: string,
|
||||
topic: string
|
||||
};
|
||||
|
||||
declare type Control = {
|
||||
name: string,
|
||||
position: Array<number>,
|
||||
icon: string,
|
||||
ui: Array<ControlUI>
|
||||
};
|
||||
declare type Controls = Map<string,Control>;
|
||||
|
||||
declare type Config = {
|
||||
topics: Topics,
|
||||
controls: Controls
|
||||
};
|
||||
|
||||
declare type State = {
|
||||
mqtt: ?any,
|
||||
ui: ?string,
|
||||
values: Map<string,any>
|
||||
};
|
||||
25
webpack.config.js
Normal file
25
webpack.config.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// webpack.config.js:
|
||||
|
||||
var path = require('path');
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
entry: [
|
||||
path.resolve(__dirname, 'src/index.jsx')
|
||||
],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'public/dist'),
|
||||
filename: 'main.js',
|
||||
publicPath: 'dist/'
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.css$/, loader: "style!css" },
|
||||
{ test: /\.js(x)?$/, exclude: /node_modules/, loader: "babel-loader" }
|
||||
]
|
||||
},
|
||||
devtool: 'source-map'
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue