Added Navbar

This commit is contained in:
Richárd Kunkli 2020-10-24 17:00:44 +02:00
parent 0f0e5d9d1c
commit 96003c21dd
24 changed files with 230 additions and 416 deletions

View File

@ -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') ">

View File

@ -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=="
} }
} }
}, },

View File

@ -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>
@ -91,3 +113,34 @@ const DefaultLayout = ({ component: Component, ...rest }: { [x: string]: any, co
</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',
}
},
}),
);

View File

@ -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"}

View File

@ -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)

View 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',
}
},
}),
);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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>
);

View File

@ -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); }

View File

@ -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
});
}
}

View File

@ -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",

View File

@ -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;

View File

@ -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"}

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}
};

View File

@ -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;
};

View 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

View 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"}

View 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

View 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"}

View File

@ -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