Added Navbar
This commit is contained in:
parent
0f0e5d9d1c
commit
96003c21dd
@ -26,6 +26,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="ClientApp\src\common\components\BirdmapTitle.tsx" />
|
||||||
<None Remove="ClientApp\src\common\ErrorDispatcher.ts" />
|
<None Remove="ClientApp\src\common\ErrorDispatcher.ts" />
|
||||||
<None Remove="ClientApp\src\common\ServiceBase.ts" />
|
<None Remove="ClientApp\src\common\ServiceBase.ts" />
|
||||||
<None Remove="ClientApp\src\components\auth\Auth.tsx" />
|
<None Remove="ClientApp\src\components\auth\Auth.tsx" />
|
||||||
@ -34,6 +35,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<TypeScriptCompile Include="ClientApp\src\components\auth\Auth.tsx" />
|
<TypeScriptCompile Include="ClientApp\src\components\auth\Auth.tsx" />
|
||||||
|
<TypeScriptCompile Include="ClientApp\src\common\components\BirdmapTitle.tsx" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="ClientApp\src\components\dashboard\" />
|
||||||
|
<Folder Include="ClientApp\src\components\devices\" />
|
||||||
|
<Folder Include="ClientApp\src\components\heatmap\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||||
|
12
Birdmap/ClientApp/package-lock.json
generated
12
Birdmap/ClientApp/package-lock.json
generated
@ -1444,9 +1444,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz",
|
||||||
"integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag=="
|
"integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA=="
|
||||||
},
|
},
|
||||||
"dom-helpers": {
|
"dom-helpers": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
@ -7933,9 +7933,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz",
|
||||||
"integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag=="
|
"integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,45 +1,63 @@
|
|||||||
import { Box, Container } from '@material-ui/core';
|
import { Box, Container } from '@material-ui/core';
|
||||||
import AppBar from '@material-ui/core/AppBar';
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
import blue from '@material-ui/core/colors/blue';
|
import blue from '@material-ui/core/colors/blue';
|
||||||
import { createMuiTheme } from '@material-ui/core/styles';
|
import orange from '@material-ui/core/colors/orange';
|
||||||
|
import { createMuiTheme, createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||||
import Toolbar from '@material-ui/core/Toolbar';
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
import Typography from '@material-ui/core/Typography';
|
import Typography from '@material-ui/core/Typography';
|
||||||
import { ThemeProvider } from '@material-ui/styles';
|
import { ThemeProvider } from '@material-ui/styles';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { BrowserRouter, NavLink, Redirect, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter, NavLink, Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
import BirdmapTitle from './common/components/BirdmapTitle';
|
||||||
import Auth from './components/auth/Auth';
|
import Auth from './components/auth/Auth';
|
||||||
import AuthService from './components/auth/AuthService';
|
import AuthService from './components/auth/AuthService';
|
||||||
import Home from './components/Home';
|
|
||||||
import './custom.css';
|
|
||||||
|
|
||||||
|
|
||||||
const theme = createMuiTheme({
|
const theme = createMuiTheme({
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: blue[800]
|
main: blue[900],
|
||||||
},
|
},
|
||||||
|
secondary: {
|
||||||
|
main: orange[200],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const [authenticated, setAuthenticated] = useState(AuthService.isAuthenticated());
|
const [authenticated, setAuthenticated] = useState(AuthService.isAuthenticated());
|
||||||
|
|
||||||
const onAuthenticated = () => {
|
const onAuthenticated = () => {
|
||||||
setAuthenticated(AuthService.isAuthenticated());
|
setAuthenticated(AuthService.isAuthenticated());
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoginComponent = () => {
|
const AuthComponent = () => {
|
||||||
return (
|
return (
|
||||||
<Auth onAuthenticated={onAuthenticated} />
|
<Auth onAuthenticated={onAuthenticated} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DashboardComponent = () => {
|
||||||
|
return <Typography>Dashboard</Typography>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DevicesComponent = () => {
|
||||||
|
return <Typography>Devices</Typography>;
|
||||||
|
|
||||||
|
};
|
||||||
|
const HeatmapComponent = () => {
|
||||||
|
return <Typography>Heatmap</Typography>;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<PublicRoute path="/login" component={LoginComponent} />
|
<PublicRoute path="/login" component={AuthComponent} />
|
||||||
<PrivateRoute path="/" exact authenticated={authenticated} component={Home} />
|
<PrivateRoute path="/" exact authenticated={authenticated} component={DashboardComponent} />
|
||||||
|
<PrivateRoute path="/devices" exact authenticated={authenticated} component={DevicesComponent} />
|
||||||
|
<PrivateRoute path="/heatmap" exact authenticated={authenticated} component={HeatmapComponent} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@ -51,7 +69,7 @@ export default App;
|
|||||||
const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any}) => {
|
const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any}) => {
|
||||||
return (
|
return (
|
||||||
<Route {...rest} render={matchProps => (
|
<Route {...rest} render={matchProps => (
|
||||||
<NoLayout component={Component} {...matchProps} />
|
<DefaultLayout component={Component} authenticated={false} {...matchProps} />
|
||||||
)} />
|
)} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,28 +78,32 @@ const PrivateRoute = ({ component: Component, authenticated: Authenticated, ...r
|
|||||||
return (
|
return (
|
||||||
<Route {...rest} render={matchProps => (
|
<Route {...rest} render={matchProps => (
|
||||||
Authenticated
|
Authenticated
|
||||||
? <DefaultLayout component={Component} {...matchProps} />
|
? <DefaultLayout component={Component} authenticated={Authenticated} {...matchProps} />
|
||||||
: <Redirect to='/login' />
|
: <Redirect to='/login' />
|
||||||
)} />
|
)} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NoLayout = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
|
const DefaultLayout = ({ component: Component, authenticated: Authenticated, ...rest }: { [x: string]: any, component: any, authenticated: any }) => {
|
||||||
return (
|
const classes = useDefaultLayoutStyles();
|
||||||
<Component {...rest} />
|
|
||||||
);
|
const renderNavLinks = () => {
|
||||||
};
|
return Authenticated
|
||||||
|
? <Container className={classes.nav_menu}>
|
||||||
|
<NavLink exact to="/" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Dashboard</NavLink>
|
||||||
|
<NavLink exact to="/devices" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Devices</NavLink>
|
||||||
|
<NavLink exact to="/heatmap" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Heatmap</NavLink>
|
||||||
|
</Container>
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
const DefaultLayout = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<AppBar position="static">
|
<AppBar position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<Typography component={'span'}>
|
<BirdmapTitle />
|
||||||
<Container className="nav-menu">
|
<Typography component={'span'} className={classes.typo}>
|
||||||
<NavLink exact to="/" className="nav-menu-item" activeStyle={{ color: 'white' }}>Dashboard</NavLink>
|
{renderNavLinks()}
|
||||||
<NavLink exact to="/login" className="nav-menu-item" activeStyle={{ color: 'white' }}>Login</NavLink>
|
|
||||||
</Container>
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
@ -90,4 +112,35 @@ const DefaultLayout = ({ component: Component, ...rest }: { [x: string]: any, co
|
|||||||
</Box>
|
</Box>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useDefaultLayoutStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
typo: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
nav_menu: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
nav_menu_item: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontWeight: 'normal',
|
||||||
|
color: 'inherit',
|
||||||
|
marginLeft: '24px',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'inherit',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nav_menu_item_active: {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'inherit',
|
||||||
|
marginLeft: '24px',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'inherit',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
@ -1 +1 @@
|
|||||||
{"version":3,"file":"ServiceBase.js","sourceRoot":"","sources":["ServiceBase.ts"],"names":[],"mappings":";;AAAA,qDAAgD;AAEhD,SAAS,GAAG,CAAC,GAAG;IACZ,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;KACJ,CAAC;IAEF,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,IAAI,CAAC,GAAG,EAAE,OAAO;IACtB,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;QACD,IAAI,EAAE,EAAE;KACX,CAAC;IAEF,IAAI,OAAO;QACP,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAE3C,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,GAAG,EAAE,OAAO;IAC7B,OAAO,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC;SACrB,IAAI,CAAC,qBAAqB,CAAC;SAC3B,KAAK,CAAC,YAAY,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAQ;IACnC,IAAI,CAAC,QAAQ,CAAC,EAAE;QACZ,OAAO,QAAQ,CAAC,IAAI,EAAE;aACjB,IAAI,CAAC,UAAA,IAAI,IAAI,OAAA,YAAY,CAAC,IAAI,CAAC,EAAlB,CAAkB,CAAC,CAAC;IAE1C,OAAO,QAAQ,CAAC,IAAI,EAAE;SACjB,IAAI,CAAC,UAAA,IAAI,IAAI,OAAA,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAnC,CAAmC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,YAAY,CAAC,QAAQ;IAC1B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtB,IAAI,QAAQ,IAAI,QAAQ,CAAC,KAAK;QAC1B,yBAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE/C,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED,kBAAe;IACX,GAAG,KAAA;IACH,IAAI,MAAA;IACJ,WAAW,aAAA;CACd,CAAC"}
|
{"version":3,"file":"ServiceBase.js","sourceRoot":"","sources":["ServiceBase.ts"],"names":[],"mappings":";;AAAA,qDAAgD;AAEhD,SAAS,GAAG,CAAC,GAAW;IACpB,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;KACJ,CAAC;IAEF,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,OAAY;IACnC,IAAI,OAAO,GAAG;QACV,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACL,cAAc,EAAE,kBAAkB;YAClC,eAAe,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC;SAClD;QACD,IAAI,EAAE,EAAE;KACX,CAAC;IAEF,IAAI,OAAO;QACP,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAE3C,OAAO,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,GAAW,EAAE,OAAY;IAC1C,OAAO,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC;SACrB,IAAI,CAAC,qBAAqB,CAAC;SAC3B,KAAK,CAAC,YAAY,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,qBAAqB,CAAC,QAAa;IACxC,IAAI,CAAC,QAAQ,CAAC,EAAE;QACZ,OAAO,QAAQ,CAAC,IAAI,EAAE;aACjB,IAAI,CAAC,UAAC,IAAS,IAAK,OAAA,YAAY,CAAC,IAAI,CAAC,EAAlB,CAAkB,CAAC,CAAC;IAEjD,OAAO,QAAQ,CAAC,IAAI,EAAE;SACjB,IAAI,CAAC,UAAC,IAAS,IAAK,OAAA,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,EAAnC,CAAmC,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,YAAY,CAAC,QAAa;IAC/B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEtB,IAAI,QAAQ,IAAI,QAAQ,CAAC,KAAK;QAC1B,yBAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE/C,OAAO,OAAO,CAAC,MAAM,EAAE,CAAC;AAC5B,CAAC;AAED,kBAAe;IACX,GAAG,KAAA;IACH,IAAI,MAAA;IACJ,WAAW,aAAA;CACd,CAAC"}
|
@ -1,6 +1,6 @@
|
|||||||
import ErrorDispatcher from './ErrorDispatcher';
|
import ErrorDispatcher from './ErrorDispatcher';
|
||||||
|
|
||||||
function get(url) {
|
function get(url: string) {
|
||||||
let options = {
|
let options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@ -12,7 +12,7 @@ function get(url) {
|
|||||||
return makeRequest(url, options);
|
return makeRequest(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function post(url, request) {
|
function post(url: string, request: any) {
|
||||||
let options = {
|
let options = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -28,22 +28,22 @@ function post(url, request) {
|
|||||||
return makeRequest(url, options);
|
return makeRequest(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRequest(url, options) {
|
function makeRequest(url: string, options: any) {
|
||||||
return fetch(url, options)
|
return fetch(url, options)
|
||||||
.then(ensureResponseSuccess)
|
.then(ensureResponseSuccess)
|
||||||
.catch(errorHandler);
|
.catch(errorHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureResponseSuccess(response) {
|
function ensureResponseSuccess(response: any) {
|
||||||
if (!response.ok)
|
if (!response.ok)
|
||||||
return response.json()
|
return response.json()
|
||||||
.then(data => errorHandler(data));
|
.then((data: any) => errorHandler(data));
|
||||||
|
|
||||||
return response.text()
|
return response.text()
|
||||||
.then(text => text.length ? JSON.parse(text) : {});
|
.then((text: any) => text.length ? JSON.parse(text) : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorHandler(response) {
|
function errorHandler(response: any) {
|
||||||
console.log(response);
|
console.log(response);
|
||||||
|
|
||||||
if (response && response.Error)
|
if (response && response.Error)
|
||||||
|
59
Birdmap/ClientApp/src/common/components/BirdmapTitle.tsx
Normal file
59
Birdmap/ClientApp/src/common/components/BirdmapTitle.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Box, Typography } from '@material-ui/core';
|
||||||
|
import { BrowserRouter, NavLink, Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function BirdmapTitle(props: any) {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="span" className={classes.root}>
|
||||||
|
<Typography component="span" className={classes.bird}>
|
||||||
|
<NavLink exact to="/" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>
|
||||||
|
Bird
|
||||||
|
</NavLink>
|
||||||
|
</Typography>
|
||||||
|
<Typography component="span" className={classes.map}>
|
||||||
|
<NavLink exact to="/heatmap" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>
|
||||||
|
map
|
||||||
|
</NavLink>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
|
root: {
|
||||||
|
display: 'inline',
|
||||||
|
},
|
||||||
|
bird: {
|
||||||
|
textAlign: "left",
|
||||||
|
fontWeight: 1000,
|
||||||
|
fontSize: 30,
|
||||||
|
textShadow: '3px 3px 0px rgba(0,0,0,0.2)',
|
||||||
|
},
|
||||||
|
map: {
|
||||||
|
textAlign: "left",
|
||||||
|
fontWeight: 100,
|
||||||
|
fontSize: 26,
|
||||||
|
textShadow: '2px 2px 0px rgba(0,0,0,0.2)',
|
||||||
|
},
|
||||||
|
nav_menu_item: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
color: 'white',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nav_menu_item_active: {
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
color: 'white',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
@ -1,35 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { RouteComponentProps } from 'react-router';
|
|
||||||
import { ApplicationState } from '../store';
|
|
||||||
import * as CounterStore from '../store/Counter';
|
|
||||||
|
|
||||||
type CounterProps =
|
|
||||||
CounterStore.CounterState &
|
|
||||||
typeof CounterStore.actionCreators &
|
|
||||||
RouteComponentProps<{}>;
|
|
||||||
|
|
||||||
class Counter extends React.PureComponent<CounterProps> {
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1>Counter</h1>
|
|
||||||
|
|
||||||
<p>This is a simple example of a React component.</p>
|
|
||||||
|
|
||||||
<p aria-live="polite">Current count: <strong>{this.props.count}</strong></p>
|
|
||||||
|
|
||||||
<button type="button"
|
|
||||||
className="btn btn-primary btn-lg"
|
|
||||||
onClick={() => { this.props.increment(); }}>
|
|
||||||
Increment
|
|
||||||
</button>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
(state: ApplicationState) => state.counter,
|
|
||||||
CounterStore.actionCreators
|
|
||||||
)(Counter);
|
|
@ -1,84 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { RouteComponentProps } from 'react-router';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { ApplicationState } from '../store';
|
|
||||||
import * as WeatherForecastsStore from '../store/WeatherForecasts';
|
|
||||||
|
|
||||||
// At runtime, Redux will merge together...
|
|
||||||
type WeatherForecastProps =
|
|
||||||
WeatherForecastsStore.WeatherForecastsState // ... state we've requested from the Redux store
|
|
||||||
& typeof WeatherForecastsStore.actionCreators // ... plus action creators we've requested
|
|
||||||
& RouteComponentProps<{ startDateIndex: string }>; // ... plus incoming routing parameters
|
|
||||||
|
|
||||||
|
|
||||||
class FetchData extends React.PureComponent<WeatherForecastProps> {
|
|
||||||
// This method is called when the component is first added to the document
|
|
||||||
public componentDidMount() {
|
|
||||||
this.ensureDataFetched();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This method is called when the route parameters change
|
|
||||||
public componentDidUpdate() {
|
|
||||||
this.ensureDataFetched();
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<h1 id="tabelLabel">Weather forecast</h1>
|
|
||||||
<p>This component demonstrates fetching data from the server and working with URL parameters.</p>
|
|
||||||
{this.renderForecastsTable()}
|
|
||||||
{this.renderPagination()}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ensureDataFetched() {
|
|
||||||
const startDateIndex = parseInt(this.props.match.params.startDateIndex, 10) || 0;
|
|
||||||
this.props.requestWeatherForecasts(startDateIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderForecastsTable() {
|
|
||||||
return (
|
|
||||||
<table className='table table-striped' aria-labelledby="tabelLabel">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Temp. (C)</th>
|
|
||||||
<th>Temp. (F)</th>
|
|
||||||
<th>Summary</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{this.props.forecasts.map((forecast: WeatherForecastsStore.WeatherForecast) =>
|
|
||||||
<tr key={forecast.date}>
|
|
||||||
<td>{forecast.date}</td>
|
|
||||||
<td>{forecast.temperatureC}</td>
|
|
||||||
<td>{forecast.temperatureF}</td>
|
|
||||||
<td>{forecast.summary}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPagination() {
|
|
||||||
const prevStartDateIndex = (this.props.startDateIndex || 0) - 5;
|
|
||||||
const nextStartDateIndex = (this.props.startDateIndex || 0) + 5;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="d-flex justify-content-between">
|
|
||||||
<Link className='btn btn-outline-secondary btn-sm' to={`/fetch-data/${prevStartDateIndex}`}>Previous</Link>
|
|
||||||
{this.props.isLoading && <span>Loading...</span>}
|
|
||||||
<Link className='btn btn-outline-secondary btn-sm' to={`/fetch-data/${nextStartDateIndex}`}>Next</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
(state: ApplicationState) => state.weatherForecasts, // Selects which state properties are merged into the component's props
|
|
||||||
WeatherForecastsStore.actionCreators // Selects which action creators are merged into the component's props
|
|
||||||
)(FetchData as any);
|
|
@ -1,23 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
const Home = () => (
|
|
||||||
<div>
|
|
||||||
<h1>Hello, world!</h1>
|
|
||||||
<p>Welcome to your new single-page application, built with:</p>
|
|
||||||
<ul>
|
|
||||||
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
|
|
||||||
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
|
|
||||||
<li><a href='http://getbootstrap.com/'>Bootstrap</a> for layout and styling</li>
|
|
||||||
</ul>
|
|
||||||
<p>To help you get started, we've also set up:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
|
|
||||||
<li><strong>Development server integration</strong>. In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.</li>
|
|
||||||
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files.</li>
|
|
||||||
</ul>
|
|
||||||
<p>The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>npm</code> commands such as <code>npm test</code> or <code>npm install</code>.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default connect()(Home);
|
|
@ -1,12 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Container } from 'reactstrap';
|
|
||||||
import NavMenu from './NavMenu';
|
|
||||||
|
|
||||||
export default (props: { children?: React.ReactNode }) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<NavMenu/>
|
|
||||||
<Container>
|
|
||||||
{props.children}
|
|
||||||
</Container>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
@ -1,13 +0,0 @@
|
|||||||
a.navbar-brand {
|
|
||||||
white-space: normal;
|
|
||||||
text-align: center;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
html { font-size: 14px; }
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
html { font-size: 16px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
|
|
@ -1,42 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import './NavMenu.css';
|
|
||||||
|
|
||||||
export default class NavMenu extends React.PureComponent<{}, { isOpen: boolean }> {
|
|
||||||
public state = {
|
|
||||||
isOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<header>
|
|
||||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm border-bottom box-shadow mb-3" light>
|
|
||||||
<Container>
|
|
||||||
<NavbarBrand tag={Link} to="/">Birdmap</NavbarBrand>
|
|
||||||
<NavbarToggler onClick={this.toggle} className="mr-2"/>
|
|
||||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={this.state.isOpen} navbar>
|
|
||||||
<ul className="navbar-nav flex-grow">
|
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} className="text-dark" to="/">Home</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} className="text-dark" to="/counter">Counter</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
<NavItem>
|
|
||||||
<NavLink tag={Link} className="text-dark" to="/fetch-data">Fetch data</NavLink>
|
|
||||||
</NavItem>
|
|
||||||
</ul>
|
|
||||||
</Collapse>
|
|
||||||
</Container>
|
|
||||||
</Navbar>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggle = () => {
|
|
||||||
this.setState({
|
|
||||||
isOpen: !this.state.isOpen
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +1,19 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Box, Grid, TextField, Button, Typography, Paper } from '@material-ui/core';
|
import { Box, Grid, TextField, Button, Typography, Paper, CircularProgress } from '@material-ui/core';
|
||||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
||||||
import AuthService from './AuthService';
|
import AuthService from './AuthService';
|
||||||
|
|
||||||
export default function MenuItem(props: any) {
|
export default function Auth(props: any) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const [username, setUsername] = useState(null);
|
const [username, setUsername] = useState<string>("");
|
||||||
const [password, setPassword] = useState(null);
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
const [showError, setShowError] = useState(false);
|
const [showError, setShowError] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
|
||||||
const onUsernameChanged = (event: any) => {
|
const onUsernameChanged = (event: any) => {
|
||||||
setUsername(event.target.value);
|
setUsername(event.target.value);
|
||||||
@ -35,6 +36,8 @@ export default function MenuItem(props: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onLoginClicked = () => {
|
const onLoginClicked = () => {
|
||||||
|
setIsLoggingIn(true);
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
setShowError(true);
|
setShowError(true);
|
||||||
setErrorMessage('Username required');
|
setErrorMessage('Username required');
|
||||||
@ -49,13 +52,15 @@ export default function MenuItem(props: any) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthService.login(username, password).then(() => {
|
AuthService.login(username, password)
|
||||||
|
.then(() => {
|
||||||
props.onAuthenticated();
|
props.onAuthenticated();
|
||||||
history.push('/');
|
history.push('/');
|
||||||
|
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setShowError(true);
|
setShowError(true);
|
||||||
setErrorMessage('Invalid credentials');
|
setErrorMessage('Invalid credentials');
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoggingIn(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -65,9 +70,15 @@ export default function MenuItem(props: any) {
|
|||||||
: <React.Fragment />;
|
: <React.Fragment />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderLoginButton = () => {
|
||||||
|
return isLoggingIn
|
||||||
|
? <CircularProgress className={classes.button} />
|
||||||
|
: <Button className={classes.button} variant="contained" color="primary" onClick={onLoginClicked}>Sign in</Button>
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.root}>
|
<Box className={classes.root}>
|
||||||
<Paper className={classes.paper}>
|
<Paper className={classes.paper} elevation={8}>
|
||||||
<Grid container className={classes.container} spacing={2}>
|
<Grid container className={classes.container} spacing={2}>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography component="h1" variant="h5">
|
<Typography component="h1" variant="h5">
|
||||||
@ -84,7 +95,7 @@ export default function MenuItem(props: any) {
|
|||||||
{renderErrorLabel()}
|
{renderErrorLabel()}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} className={classes.button}>
|
<Grid item xs={12} className={classes.button}>
|
||||||
<Button className={classes.button} variant="contained" color="primary" onClick={onLoginClicked}>Login</Button>
|
{renderLoginButton()}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Paper>
|
</Paper>
|
||||||
@ -108,9 +119,10 @@ const useStyles = makeStyles((theme: Theme) =>
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
paper: {
|
paper: {
|
||||||
|
borderRadius: 15,
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
width: '100%',
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: "red",
|
color: "red",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
var ServiceBase_1 = require("../../common/ServiceBase");
|
var ServiceBase_1 = require("../../common/ServiceBase");
|
||||||
var login_url = '/auth/authenticate';
|
var login_url = 'api/auth/authenticate';
|
||||||
exports.default = {
|
exports.default = {
|
||||||
isAuthenticated: function () {
|
isAuthenticated: function () {
|
||||||
return sessionStorage.getItem('user') !== null;
|
return sessionStorage.getItem('user') !== null;
|
||||||
|
@ -1 +1 @@
|
|||||||
{"version":3,"file":"AuthService.js","sourceRoot":"","sources":["AuthService.ts"],"names":[],"mappings":";;AAAA,wDAAmD;AAEnD,IAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,kBAAe;IACX,eAAe;QACX,OAAO,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,YAAC,QAAQ,EAAE,QAAQ;QACpB,IAAI,IAAI,GAAG;YACP,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,IAAI,OAAO,GAAG;YACV,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,kBAAkB;aACrC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC7B,CAAC;QAEF,OAAO,qBAAW,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC;aAC7C,IAAI,CAAC,UAAA,QAAQ;YACV,cAAc,CAAC,OAAO,CAAC,MAAM,EAAK,QAAQ,CAAC,UAAU,SAAI,QAAQ,CAAC,YAAc,CAAC,CAAC;YAClF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;IACX,CAAC;CACJ,CAAA"}
|
{"version":3,"file":"AuthService.js","sourceRoot":"","sources":["AuthService.ts"],"names":[],"mappings":";;AAAA,wDAAmD;AAEnD,IAAM,SAAS,GAAG,uBAAuB,CAAC;AAE1C,kBAAe;IACX,eAAe;QACX,OAAO,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IACnD,CAAC;IAED,KAAK,EAAL,UAAM,QAAgB,EAAE,QAAgB;QACpC,IAAI,IAAI,GAAG;YACP,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,QAAQ;SACrB,CAAC;QACF,IAAI,OAAO,GAAG;YACV,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACL,cAAc,EAAE,kBAAkB;aACrC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC7B,CAAC;QAEF,OAAO,qBAAW,CAAC,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC;aAC7C,IAAI,CAAC,UAAA,QAAQ;YACV,cAAc,CAAC,OAAO,CAAC,MAAM,EAAK,QAAQ,CAAC,UAAU,SAAI,QAAQ,CAAC,YAAc,CAAC,CAAC;YAClF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC7B,CAAC,CAAC,CAAC;IACX,CAAC;CACJ,CAAA"}
|
@ -1,13 +1,13 @@
|
|||||||
import ServiceBase from '../../common/ServiceBase';
|
import ServiceBase from '../../common/ServiceBase';
|
||||||
|
|
||||||
const login_url = '/auth/authenticate';
|
const login_url = 'api/auth/authenticate';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
isAuthenticated() {
|
isAuthenticated() {
|
||||||
return sessionStorage.getItem('user') !== null;
|
return sessionStorage.getItem('user') !== null;
|
||||||
},
|
},
|
||||||
|
|
||||||
login(username, password) {
|
login(username: string, password: string) {
|
||||||
let body = {
|
let body = {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
/* Provide sufficient contrast against white background */
|
|
||||||
a {
|
|
||||||
color: #0366d6;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: #E01A76;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #1b6ec2;
|
|
||||||
border-color: #1861ac;
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import { Action, Reducer } from 'redux';
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
// STATE - This defines the type of data maintained in the Redux store.
|
|
||||||
|
|
||||||
export interface CounterState {
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
|
|
||||||
// They do not themselves have any side-effects; they just describe something that is going to happen.
|
|
||||||
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.
|
|
||||||
|
|
||||||
export interface IncrementCountAction { type: 'INCREMENT_COUNT' }
|
|
||||||
export interface DecrementCountAction { type: 'DECREMENT_COUNT' }
|
|
||||||
|
|
||||||
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
|
|
||||||
// declared type strings (and not any other arbitrary string).
|
|
||||||
export type KnownAction = IncrementCountAction | DecrementCountAction;
|
|
||||||
|
|
||||||
// ----------------
|
|
||||||
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
|
|
||||||
// They don't directly mutate state, but they can have external side-effects (such as loading data).
|
|
||||||
|
|
||||||
export const actionCreators = {
|
|
||||||
increment: () => ({ type: 'INCREMENT_COUNT' } as IncrementCountAction),
|
|
||||||
decrement: () => ({ type: 'DECREMENT_COUNT' } as DecrementCountAction)
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------
|
|
||||||
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
|
|
||||||
|
|
||||||
export const reducer: Reducer<CounterState> = (state: CounterState | undefined, incomingAction: Action): CounterState => {
|
|
||||||
if (state === undefined) {
|
|
||||||
return { count: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = incomingAction as KnownAction;
|
|
||||||
switch (action.type) {
|
|
||||||
case 'INCREMENT_COUNT':
|
|
||||||
return { count: state.count + 1 };
|
|
||||||
case 'DECREMENT_COUNT':
|
|
||||||
return { count: state.count - 1 };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,91 +0,0 @@
|
|||||||
import { Action, Reducer } from 'redux';
|
|
||||||
import { AppThunkAction } from './';
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
// STATE - This defines the type of data maintained in the Redux store.
|
|
||||||
|
|
||||||
export interface WeatherForecastsState {
|
|
||||||
isLoading: boolean;
|
|
||||||
startDateIndex?: number;
|
|
||||||
forecasts: WeatherForecast[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WeatherForecast {
|
|
||||||
date: string;
|
|
||||||
temperatureC: number;
|
|
||||||
temperatureF: number;
|
|
||||||
summary: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------
|
|
||||||
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
|
|
||||||
// They do not themselves have any side-effects; they just describe something that is going to happen.
|
|
||||||
|
|
||||||
interface RequestWeatherForecastsAction {
|
|
||||||
type: 'REQUEST_WEATHER_FORECASTS';
|
|
||||||
startDateIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ReceiveWeatherForecastsAction {
|
|
||||||
type: 'RECEIVE_WEATHER_FORECASTS';
|
|
||||||
startDateIndex: number;
|
|
||||||
forecasts: WeatherForecast[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
|
|
||||||
// declared type strings (and not any other arbitrary string).
|
|
||||||
type KnownAction = RequestWeatherForecastsAction | ReceiveWeatherForecastsAction;
|
|
||||||
|
|
||||||
// ----------------
|
|
||||||
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
|
|
||||||
// They don't directly mutate state, but they can have external side-effects (such as loading data).
|
|
||||||
|
|
||||||
export const actionCreators = {
|
|
||||||
requestWeatherForecasts: (startDateIndex: number): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
|
||||||
// Only load data if it's something we don't already have (and are not already loading)
|
|
||||||
const appState = getState();
|
|
||||||
if (appState && appState.weatherForecasts && startDateIndex !== appState.weatherForecasts.startDateIndex) {
|
|
||||||
fetch(`weatherforecast`)
|
|
||||||
.then(response => response.json() as Promise<WeatherForecast[]>)
|
|
||||||
.then(data => {
|
|
||||||
dispatch({ type: 'RECEIVE_WEATHER_FORECASTS', startDateIndex: startDateIndex, forecasts: data });
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({ type: 'REQUEST_WEATHER_FORECASTS', startDateIndex: startDateIndex });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------
|
|
||||||
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
|
|
||||||
|
|
||||||
const unloadedState: WeatherForecastsState = { forecasts: [], isLoading: false };
|
|
||||||
|
|
||||||
export const reducer: Reducer<WeatherForecastsState> = (state: WeatherForecastsState | undefined, incomingAction: Action): WeatherForecastsState => {
|
|
||||||
if (state === undefined) {
|
|
||||||
return unloadedState;
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = incomingAction as KnownAction;
|
|
||||||
switch (action.type) {
|
|
||||||
case 'REQUEST_WEATHER_FORECASTS':
|
|
||||||
return {
|
|
||||||
startDateIndex: action.startDateIndex,
|
|
||||||
forecasts: state.forecasts,
|
|
||||||
isLoading: true
|
|
||||||
};
|
|
||||||
case 'RECEIVE_WEATHER_FORECASTS':
|
|
||||||
// Only accept the incoming data if it matches the most recent request. This ensures we correctly
|
|
||||||
// handle out-of-order responses.
|
|
||||||
if (action.startDateIndex === state.startDateIndex) {
|
|
||||||
return {
|
|
||||||
startDateIndex: action.startDateIndex,
|
|
||||||
forecasts: action.forecasts,
|
|
||||||
isLoading: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
39
Birdmap/ClientApp/src/store/configureStore.js
Normal file
39
Birdmap/ClientApp/src/store/configureStore.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
var __assign = (this && this.__assign) || function () {
|
||||||
|
__assign = Object.assign || function(t) {
|
||||||
|
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||||
|
s = arguments[i];
|
||||||
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||||
|
t[p] = s[p];
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
return __assign.apply(this, arguments);
|
||||||
|
};
|
||||||
|
var __spreadArrays = (this && this.__spreadArrays) || function () {
|
||||||
|
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
|
||||||
|
for (var r = Array(s), k = 0, i = 0; i < il; i++)
|
||||||
|
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
|
||||||
|
r[k] = a[j];
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
var redux_1 = require("redux");
|
||||||
|
var redux_thunk_1 = require("redux-thunk");
|
||||||
|
var connected_react_router_1 = require("connected-react-router");
|
||||||
|
var _1 = require("./");
|
||||||
|
function configureStore(history, initialState) {
|
||||||
|
var middleware = [
|
||||||
|
redux_thunk_1.default,
|
||||||
|
connected_react_router_1.routerMiddleware(history)
|
||||||
|
];
|
||||||
|
var rootReducer = redux_1.combineReducers(__assign(__assign({}, _1.reducers), { router: connected_react_router_1.connectRouter(history) }));
|
||||||
|
var enhancers = [];
|
||||||
|
var windowIfDefined = typeof window === 'undefined' ? null : window;
|
||||||
|
if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||||
|
enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__());
|
||||||
|
}
|
||||||
|
return redux_1.createStore(rootReducer, initialState, redux_1.compose.apply(void 0, __spreadArrays([redux_1.applyMiddleware.apply(void 0, middleware)], enhancers)));
|
||||||
|
}
|
||||||
|
exports.default = configureStore;
|
||||||
|
//# sourceMappingURL=configureStore.js.map
|
1
Birdmap/ClientApp/src/store/configureStore.js.map
Normal file
1
Birdmap/ClientApp/src/store/configureStore.js.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"configureStore.js","sourceRoot":"","sources":["configureStore.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,+BAA+E;AAC/E,2CAAgC;AAChC,iEAAyE;AAEzE,uBAAgD;AAEhD,SAAwB,cAAc,CAAC,OAAgB,EAAE,YAA+B;IACpF,IAAM,UAAU,GAAG;QACf,qBAAK;QACL,yCAAgB,CAAC,OAAO,CAAC;KAC5B,CAAC;IAEF,IAAM,WAAW,GAAG,uBAAe,uBAC5B,WAAQ,KACX,MAAM,EAAE,sCAAa,CAAC,OAAO,CAAC,IAChC,CAAC;IAEH,IAAM,SAAS,GAAG,EAAE,CAAC;IACrB,IAAM,eAAe,GAAG,OAAO,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAa,CAAC;IAC7E,IAAI,eAAe,IAAI,eAAe,CAAC,4BAA4B,EAAE;QACjE,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,4BAA4B,EAAE,CAAC,CAAC;KAClE;IAED,OAAO,mBAAW,CACd,WAAW,EACX,YAAY,EACZ,eAAO,+BAAC,uBAAe,eAAI,UAAU,IAAM,SAAS,GACvD,CAAC;AACN,CAAC;AAtBD,iCAsBC"}
|
8
Birdmap/ClientApp/src/store/index.js
Normal file
8
Birdmap/ClientApp/src/store/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.reducers = void 0;
|
||||||
|
// Whenever an action is dispatched, Redux will update each top-level application state property using
|
||||||
|
// the reducer with the matching name. It's important that the names match exactly, and that the reducer
|
||||||
|
// acts on the corresponding ApplicationState property type.
|
||||||
|
exports.reducers = {};
|
||||||
|
//# sourceMappingURL=index.js.map
|
1
Birdmap/ClientApp/src/store/index.js.map
Normal file
1
Birdmap/ClientApp/src/store/index.js.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAMA,sGAAsG;AACtG,wGAAwG;AACxG,4DAA4D;AAC/C,QAAA,QAAQ,GAAG,EACvB,CAAC"}
|
@ -1,18 +1,13 @@
|
|||||||
import * as WeatherForecasts from './WeatherForecasts';
|
|
||||||
import * as Counter from './Counter';
|
|
||||||
|
|
||||||
// The top-level state object
|
// The top-level state object
|
||||||
export interface ApplicationState {
|
export interface ApplicationState {
|
||||||
counter: Counter.CounterState | undefined;
|
|
||||||
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whenever an action is dispatched, Redux will update each top-level application state property using
|
// Whenever an action is dispatched, Redux will update each top-level application state property using
|
||||||
// the reducer with the matching name. It's important that the names match exactly, and that the reducer
|
// the reducer with the matching name. It's important that the names match exactly, and that the reducer
|
||||||
// acts on the corresponding ApplicationState property type.
|
// acts on the corresponding ApplicationState property type.
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
counter: Counter.reducer,
|
|
||||||
weatherForecasts: WeatherForecasts.reducer
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
|
// This type can be used as a hint on action creators so that its 'dispatch' and 'getState' params are
|
||||||
|
Loading…
Reference in New Issue
Block a user