Initial commit

This commit is contained in:
2020-10-21 11:05:17 +02:00
commit 33884cdd2f
39 changed files with 18132 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

21
Birdmap/ClientApp/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

2228
Birdmap/ClientApp/README.md Normal file

File diff suppressed because it is too large Load Diff

14236
Birdmap/ClientApp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
{
"name": "birdmap",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.3.1",
"connected-react-router": "6.5.2",
"history": "4.10.1",
"jquery": "^3.5.1",
"merge": "1.2.1",
"popper.js": "^1.16.0",
"react": "16.11.0",
"react-dom": "16.11.0",
"react-redux": "7.1.1",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-scripts": "^3.4.3",
"reactstrap": "8.1.1",
"redux": "4.0.4",
"redux-thunk": "2.3.0",
"svgo": "1.3.0"
},
"devDependencies": {
"@types/history": "4.7.3",
"@types/jest": "24.0.19",
"@types/node": "12.11.6",
"@types/react": "16.9.9",
"@types/react-dom": "16.9.2",
"@types/react-redux": "7.1.5",
"@types/react-router": "5.1.2",
"@types/react-router-dom": "5.1.0",
"@types/reactstrap": "8.0.6",
"@typescript-eslint/parser": "^2.5.0",
"cross-env": "6.0.3",
"eslint": "^6.5.1",
"eslint-plugin-flowtype": "^3.13.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.16.0",
"nan": "^2.14.1",
"typescript": "3.6.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/**/*.ts ./src/**/*.tsx"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<base href="%PUBLIC_URL%/" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Birdmap</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "Birdmap",
"name": "Birdmap",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import App from './App';
it('renders without crashing', () => {
const storeFake = (state: any) => ({
default: () => {},
subscribe: () => {},
dispatch: () => {},
getState: () => ({ ...state })
});
const store = storeFake({}) as any;
ReactDOM.render(
<Provider store={store}>
<MemoryRouter>
<App/>
</MemoryRouter>
</Provider>, document.createElement('div'));
});

View File

@ -0,0 +1,16 @@
import * as React from 'react';
import { Route } from 'react-router';
import Layout from './components/Layout';
import Home from './components/Home';
import Counter from './components/Counter';
import FetchData from './components/FetchData';
import './custom.css'
export default () => (
<Layout>
<Route exact path='/' component={Home} />
<Route path='/counter' component={Counter} />
<Route path='/fetch-data/:startDateIndex?' component={FetchData} />
</Layout>
);

View File

@ -0,0 +1,35 @@
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

@ -0,0 +1,84 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,14 @@
/* Provide sufficient contrast against white background */
a {
color: #0366d6;
}
code {
color: #E01A76;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}

View File

@ -0,0 +1,27 @@
import 'bootstrap/dist/css/bootstrap.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import { createBrowserHistory } from 'history';
import configureStore from './store/configureStore';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
// Create browser history to use in the Redux store
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href') as string;
const history = createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
const store = configureStore(history);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'));
registerServiceWorker();

View File

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

View File

@ -0,0 +1,105 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const url = process.env.PUBLIC_URL as string;
const publicUrl = new URL(url, window.location.toString());
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing as ServiceWorker;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (response.status === 404 || (contentType && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

View File

@ -0,0 +1,48 @@
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

@ -0,0 +1,91 @@
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,29 @@
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import { connectRouter, routerMiddleware } from 'connected-react-router';
import { History } from 'history';
import { ApplicationState, reducers } from './';
export default function configureStore(history: History, initialState?: ApplicationState) {
const middleware = [
thunk,
routerMiddleware(history)
];
const rootReducer = combineReducers({
...reducers,
router: connectRouter(history)
});
const enhancers = [];
const windowIfDefined = typeof window === 'undefined' ? null : window as any;
if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) {
enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__());
}
return createStore(
rootReducer,
initialState,
compose(applyMiddleware(...middleware), ...enhancers)
);
}

View File

@ -0,0 +1,22 @@
import * as WeatherForecasts from './WeatherForecasts';
import * as Counter from './Counter';
// The top-level state object
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
// 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.
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
// correctly typed to match your store.
export interface AppThunkAction<TAction> {
(dispatch: (action: TAction) => void, getState: () => ApplicationState): void;
}

View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es5",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"lib": [
"es2015",
"dom"
],
"skipLibCheck": false
},
"include": [
"src"
]
}