(refactor): update all packages to latest
This commit is contained in:
parent
a99f933507
commit
fe78191893
2
.gitignore
vendored
2
.gitignore
vendored
@ -180,7 +180,7 @@ ClientBin/
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
/node_modules
|
||||
node_modules/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2022, MAKS-IT (Maksym Sadovnychyy)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
||||
This project template was bootstrapped with [dotnet new reactredux](https://docs.microsoft.com/en-us/aspnet/core/client-side/spa/react-with-redux?view=aspnetcore-6.0), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
|
||||
|
||||
```bash
|
||||
# NET6 + Redux + TypeScript template
|
||||
dotnet new reactredux
|
||||
```
|
||||
|
||||
> :warning: This repo is mirrored from [MAKS-IT/reactredux](https://git.maks-it.com/MAKS-IT/reactredux)
|
||||
|
||||
## Important changes
|
||||
|
||||
ClientApp was splitted from `NET6` webapi `Visual Studio` project and all pakages were updated to the latest version with following procedure:
|
||||
|
||||
Install `npm-check-updates` tool
|
||||
```bash
|
||||
npm install -g npm-check-updates
|
||||
```
|
||||
|
||||
Update the package.json
|
||||
```bash
|
||||
ncu --upgrade
|
||||
```
|
||||
|
||||
Validate changes applied to your package.json and run yarn install to install new versions
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
All refactored to use functional components. Due to some compatibility issues with new `react-router` version, the package [Connected react router](https://github.com/supasate/connected-react-router) were replaced with [Redux firs history](https://github.com/salvoravida/redux-first-history).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
17
clientapp/.eslintrc.json
Normal file
17
clientapp/.eslintrc.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": ["error", 2],
|
||||
"semi": ["error", "never"],
|
||||
"@typescript-eslint/semi": "off",
|
||||
"no-unexpected-multiline": "error"
|
||||
}
|
||||
}
|
||||
22
clientapp/.vscode/launch.json
vendored
Normal file
22
clientapp/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "pwa-msedge",
|
||||
"request": "launch",
|
||||
"name": "Launch Edge against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
59
clientapp/package.json
Normal file
59
clientapp/package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "react_redux_demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "5.1.3",
|
||||
"history": "5.2.0",
|
||||
"jquery": "^3.6.0",
|
||||
"merge": "^2.1.1",
|
||||
"popper.js": "^1.16.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-redux": "7.2.6",
|
||||
"react-router": "6.2.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"reactstrap": "9.0.1",
|
||||
"redux": "4.1.2",
|
||||
"redux-first-history": "^5.0.8",
|
||||
"redux-thunk": "2.4.1",
|
||||
"svgo": "2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "27.4.0",
|
||||
"@types/node": "17.0.18",
|
||||
"@types/react": "17.0.39",
|
||||
"@types/react-dom": "17.0.11",
|
||||
"@types/react-redux": "7.1.22",
|
||||
"@types/react-router": "5.1.18",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/reactstrap": "8.7.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"nan": "^2.15.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"sass": "^1.49.7",
|
||||
"typescript": "4.5.5"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"resolutions": {
|
||||
"url-parse": ">=1.5.0",
|
||||
"lodash": ">=4.17.21"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
@ -20,7 +20,7 @@
|
||||
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>xxxx</title>
|
||||
<title>react_redux_demo</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "xxxx",
|
||||
"name": "xxxx",
|
||||
"short_name": "react_redux_demo",
|
||||
"name": "react_redux_demo",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
22
clientapp/src/App.test.tsx
Normal file
22
clientapp/src/App.test.tsx
Normal 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'))
|
||||
})
|
||||
53
clientapp/src/App.tsx
Normal file
53
clientapp/src/App.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { FC, useEffect } from 'react'
|
||||
import { Route, Routes } from 'react-router'
|
||||
|
||||
//Redux
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { actionCreators as settingsActionCreators, ISettingsState, IRoute } from './store/reducers/Settings'
|
||||
|
||||
// Components
|
||||
import { DynamicLayout } from './DynamicLayout'
|
||||
import { DynamicPage } from './DynamicPage'
|
||||
|
||||
interface IRouteProp {
|
||||
path: string,
|
||||
element?: JSX.Element
|
||||
}
|
||||
|
||||
const NestedRoutes = (routes: IRoute[], tag: string) => {
|
||||
return routes.map((route: IRoute, index: number) => {
|
||||
const props: IRouteProp = {
|
||||
path: route.path
|
||||
}
|
||||
|
||||
if (route.component) {
|
||||
const page = <DynamicPage tag={route.component} />
|
||||
props.element = tag ? <DynamicLayout tag={tag}>{page}</DynamicLayout> : page
|
||||
}
|
||||
|
||||
return <Route key={index} { ...props }>{route.childRoutes ? NestedRoutes(route.childRoutes, tag) : ''}</Route>
|
||||
})
|
||||
}
|
||||
|
||||
interface IReduxState {
|
||||
settings: ISettingsState
|
||||
}
|
||||
|
||||
const App: FC = () => {
|
||||
const dispatch = useDispatch()
|
||||
const { routes } = useSelector((state: IReduxState) => state.settings)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(settingsActionCreators.requestSettings())
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
{routes.length > 0 ? <Routes>
|
||||
{NestedRoutes(routes, 'BasicLayout')}
|
||||
</Routes> : ''}
|
||||
</>
|
||||
}
|
||||
|
||||
export {
|
||||
App
|
||||
}
|
||||
29
clientapp/src/DynamicLayout.tsx
Normal file
29
clientapp/src/DynamicLayout.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import { BasicLayout } from './layouts'
|
||||
import { ILayout } from './layouts/interfaces'
|
||||
|
||||
interface ILayouts {
|
||||
[key: string]: React.FC<ILayout>;
|
||||
}
|
||||
|
||||
const layouts: ILayouts = {
|
||||
BasicLayout: BasicLayout
|
||||
}
|
||||
|
||||
export interface IDynamicLayout {
|
||||
tag: string,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const DynamicLayout: FC<IDynamicLayout> = (props) => {
|
||||
const { tag, children } = props
|
||||
|
||||
const TagName = layouts[tag]
|
||||
return <TagName>{children}</TagName>
|
||||
}
|
||||
|
||||
export {
|
||||
DynamicLayout
|
||||
}
|
||||
28
clientapp/src/DynamicPage.tsx
Normal file
28
clientapp/src/DynamicPage.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import { Home, Counter, FetchData } from './pages'
|
||||
|
||||
interface IPages {
|
||||
[key: string]: React.FC<any>;
|
||||
}
|
||||
|
||||
const pages: IPages = {
|
||||
Home: Home,
|
||||
Counter: Counter,
|
||||
FetchData: FetchData
|
||||
}
|
||||
|
||||
export interface IDynamicPage {
|
||||
tag: string
|
||||
}
|
||||
|
||||
const DynamicPage: FC<IDynamicPage> = (props) => {
|
||||
const { tag } = props
|
||||
|
||||
const TagName = pages[tag]
|
||||
return <TagName />
|
||||
}
|
||||
|
||||
export {
|
||||
DynamicPage
|
||||
}
|
||||
5
clientapp/src/functions/getKeyValue.ts
Normal file
5
clientapp/src/functions/getKeyValue.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const getKeyValue = <T extends object, U extends keyof T>(key: U) => (obj: T) => obj[key];
|
||||
|
||||
export {
|
||||
getKeyValue
|
||||
}
|
||||
5
clientapp/src/functions/index.ts
Normal file
5
clientapp/src/functions/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { getKeyValue } from './getKeyValue'
|
||||
|
||||
export {
|
||||
getKeyValue
|
||||
}
|
||||
23
clientapp/src/index.tsx
Normal file
23
clientapp/src/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom'
|
||||
import { HistoryRouter as Router } from "redux-first-history/rr6"
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
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
|
||||
// Get the application-wide store instance, prepopulating with state from the server where available.
|
||||
const { store, history } = configureStore( createBrowserHistory(window))
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<App />
|
||||
</Router>
|
||||
</Provider>,
|
||||
document.getElementById('root'))
|
||||
|
||||
registerServiceWorker()
|
||||
41
clientapp/src/layouts/basic/NavMenu/index.tsx
Normal file
41
clientapp/src/layouts/basic/NavMenu/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const NavMenu = () => {
|
||||
|
||||
const [state, hookState] = useState({
|
||||
isOpen: false
|
||||
})
|
||||
|
||||
const toggle = () => {
|
||||
hookState({
|
||||
isOpen: !state.isOpen
|
||||
})
|
||||
}
|
||||
|
||||
return <header>
|
||||
<Navbar className="navbar-expand-sm navbar-toggleable-sm border-bottom box-shadow mb-3" light>
|
||||
|
||||
<NavbarBrand tag={Link} to="/">react_redux_demo</NavbarBrand>
|
||||
<NavbarToggler onClick={toggle} className="mr-2"/>
|
||||
<Collapse className="d-sm-inline-flex flex-sm-row-reverse" isOpen={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>
|
||||
</Navbar>
|
||||
</header>
|
||||
}
|
||||
|
||||
export {
|
||||
NavMenu
|
||||
}
|
||||
21
clientapp/src/layouts/basic/index.tsx
Normal file
21
clientapp/src/layouts/basic/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React, { FC } from 'react'
|
||||
import { Container } from 'reactstrap'
|
||||
import { NavMenu } from './NavMenu'
|
||||
|
||||
import { ILayout } from '../interfaces'
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import './scss/style.scss'
|
||||
|
||||
const BasicLayout: FC<ILayout> = ({ children = null }) => {
|
||||
return <>
|
||||
<NavMenu />
|
||||
<Container>
|
||||
{children}
|
||||
</Container>
|
||||
</>
|
||||
}
|
||||
|
||||
export {
|
||||
BasicLayout
|
||||
}
|
||||
42
clientapp/src/layouts/basic/scss/style.scss
Normal file
42
clientapp/src/layouts/basic/scss/style.scss
Normal file
@ -0,0 +1,42 @@
|
||||
//colors
|
||||
$color_science_blue_approx: #0366d6;
|
||||
$color_cerise_approx: #e01a76;
|
||||
$white: #fff;
|
||||
$color_denim_approx: #1b6ec2;
|
||||
$color_fun_blue_approx: #1861ac;
|
||||
$black_5: rgba(0, 0, 0, .05);
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color_science_blue_approx;
|
||||
&.navbar-brand {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
//Instead of the line below you could use @include word-break($value)
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: $color_cerise_approx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: $white;
|
||||
background-color: $color_denim_approx;
|
||||
border-color: $color_fun_blue_approx;
|
||||
}
|
||||
|
||||
.box-shadow {
|
||||
//Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10)
|
||||
box-shadow: 0 .25rem .75rem $black_5;
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
5
clientapp/src/layouts/index.tsx
Normal file
5
clientapp/src/layouts/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { BasicLayout } from './basic'
|
||||
|
||||
export {
|
||||
BasicLayout
|
||||
}
|
||||
3
clientapp/src/layouts/interfaces.tsx
Normal file
3
clientapp/src/layouts/interfaces.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export interface ILayout {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
29
clientapp/src/pages/Counter.tsx
Normal file
29
clientapp/src/pages/Counter.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
|
||||
// Redux
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { actionCreators as counterActionCreators, CounterState } from '../store/reducers/Counter'
|
||||
|
||||
interface IReduxState {
|
||||
counter: CounterState
|
||||
}
|
||||
|
||||
const Counter = () => {
|
||||
const dispatch = useDispatch()
|
||||
const counterState = useSelector((state: IReduxState) => state.counter)
|
||||
|
||||
const increment = () => {
|
||||
dispatch(counterActionCreators.increment())
|
||||
}
|
||||
|
||||
return <>
|
||||
<h1>Counter</h1>
|
||||
<p>This is a simple example of a React component.</p>
|
||||
<p aria-live="polite">Current count: <strong>{counterState.count}</strong></p>
|
||||
<button type="button" className="btn btn-primary btn-lg" onClick={() => { increment() }}>Increment</button>
|
||||
</>
|
||||
}
|
||||
|
||||
export {
|
||||
Counter
|
||||
}
|
||||
79
clientapp/src/pages/FetchData.tsx
Normal file
79
clientapp/src/pages/FetchData.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
// React
|
||||
import React, { useEffect } from 'react'
|
||||
import { Link, useLocation, useParams } from 'react-router-dom'
|
||||
|
||||
// Redux
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { actionCreators as weatherForecastsActionCreators, WeatherForecast, WeatherForecastsState } from '../store/reducers/WeatherForecasts'
|
||||
|
||||
interface IReduxState {
|
||||
weatherForecasts: WeatherForecastsState
|
||||
}
|
||||
|
||||
type IParams = {
|
||||
startDateIndex: string
|
||||
}
|
||||
|
||||
const FetchData = () => {
|
||||
const location = useLocation()
|
||||
const params = useParams<IParams>()
|
||||
|
||||
const dispatch = useDispatch()
|
||||
const { startDateIndex, forecasts, isLoading } = useSelector((state: IReduxState) => state.weatherForecasts)
|
||||
|
||||
const ensureDataFetched = () => {
|
||||
dispatch(weatherForecastsActionCreators.requestWeatherForecasts(
|
||||
parseInt(params.startDateIndex ? params.startDateIndex : '0', 10)
|
||||
))
|
||||
}
|
||||
|
||||
// This method is called when the route parameters change
|
||||
useEffect(() => {
|
||||
ensureDataFetched()
|
||||
}, [location.pathname])
|
||||
|
||||
const 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>
|
||||
{forecasts.map((forecast: WeatherForecast) =>
|
||||
<tr key={forecast.date}>
|
||||
<td>{forecast.date}</td>
|
||||
<td>{forecast.temperatureC}</td>
|
||||
<td>{forecast.temperatureF}</td>
|
||||
<td>{forecast.summary}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
const renderPagination = () => {
|
||||
const prevStartDateIndex = (startDateIndex || 0) - 5
|
||||
const nextStartDateIndex = (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>
|
||||
{isLoading && <span>Loading...</span>}
|
||||
<Link className='btn btn-outline-secondary btn-sm' to={`/fetch-data/${nextStartDateIndex}`}>Next</Link>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <>
|
||||
<h1 id="tabelLabel">Weather forecast</h1>
|
||||
<p>This component demonstrates fetching data from the server and working with URL parameters.</p>
|
||||
{ renderForecastsTable() }
|
||||
{ renderPagination() }
|
||||
</>
|
||||
}
|
||||
|
||||
export {
|
||||
FetchData
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as React from 'react'
|
||||
|
||||
const Home = () => (
|
||||
<div>
|
||||
const Home = () => {
|
||||
|
||||
return <div>
|
||||
<h1>Hello, world!</h1>
|
||||
<p>Welcome to your new single-page application, built with:</p>
|
||||
<ul>
|
||||
@ -18,6 +18,8 @@ const Home = () => (
|
||||
</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);
|
||||
export {
|
||||
Home
|
||||
}
|
||||
9
clientapp/src/pages/index.tsx
Normal file
9
clientapp/src/pages/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Home } from './Home'
|
||||
import { Counter } from './Counter'
|
||||
import { FetchData } from './FetchData'
|
||||
|
||||
export {
|
||||
Home,
|
||||
Counter,
|
||||
FetchData
|
||||
}
|
||||
105
clientapp/src/registerServiceWorker.ts
Normal file
105
clientapp/src/registerServiceWorker.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
47
clientapp/src/store/configureStore.ts
Normal file
47
clientapp/src/store/configureStore.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import { createReduxHistoryContext, reachify } from 'redux-first-history'
|
||||
import { History } from 'history'
|
||||
import { ApplicationState, reducers } from './'
|
||||
|
||||
export default function configureStore(history: History, initialState?: ApplicationState) {
|
||||
|
||||
const {
|
||||
createReduxHistory,
|
||||
routerMiddleware,
|
||||
routerReducer
|
||||
} = createReduxHistoryContext({ history })
|
||||
|
||||
|
||||
const middleware = [
|
||||
thunk,
|
||||
routerMiddleware
|
||||
]
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
...reducers,
|
||||
router: routerReducer
|
||||
})
|
||||
|
||||
const enhancers = []
|
||||
const windowIfDefined = typeof window === 'undefined' ? null : window as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||
enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__())
|
||||
}
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
compose(applyMiddleware(...middleware), ...enhancers))
|
||||
|
||||
const reduxHistory = createReduxHistory(store)
|
||||
|
||||
//if you use @reach/router
|
||||
const reachHistory = reachify(reduxHistory)
|
||||
|
||||
return {
|
||||
store,
|
||||
history: reduxHistory,
|
||||
reachHistory
|
||||
}
|
||||
}
|
||||
@ -1,22 +1,25 @@
|
||||
import * as WeatherForecasts from './WeatherForecasts';
|
||||
import * as Counter from './Counter';
|
||||
import * as WeatherForecasts from './reducers/WeatherForecasts'
|
||||
import * as Counter from './reducers/Counter'
|
||||
import * as Settings from './reducers/Settings'
|
||||
|
||||
// The top-level state object
|
||||
export interface ApplicationState {
|
||||
counter: Counter.CounterState | undefined;
|
||||
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined;
|
||||
counter: Counter.CounterState | undefined
|
||||
weatherForecasts: WeatherForecasts.WeatherForecastsState | undefined
|
||||
settings: Settings.ISettingsState | 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
|
||||
};
|
||||
counter: Counter.reducer,
|
||||
weatherForecasts: WeatherForecasts.reducer,
|
||||
settings: Settings.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;
|
||||
(dispatch: (action: TAction) => void, getState: () => ApplicationState): void
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
import { Action, Reducer } from 'redux';
|
||||
import { Action, Reducer } from 'redux'
|
||||
|
||||
// -----------------
|
||||
// STATE - This defines the type of data maintained in the Redux store.
|
||||
|
||||
export interface CounterState {
|
||||
count: number;
|
||||
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.
|
||||
// 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' }
|
||||
@ -17,32 +17,32 @@ 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;
|
||||
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)
|
||||
};
|
||||
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 };
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
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
|
||||
}
|
||||
}
|
||||
77
clientapp/src/store/reducers/Settings.ts
Normal file
77
clientapp/src/store/reducers/Settings.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { Action, Reducer } from 'redux'
|
||||
import { AppThunkAction } from '../'
|
||||
|
||||
export interface IRoute {
|
||||
path: string,
|
||||
component?: string,
|
||||
childRoutes?: IRoute[]
|
||||
}
|
||||
|
||||
export interface ISettingsState {
|
||||
routes: IRoute[],
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
interface RequestSettingsAction {
|
||||
type: 'REQUEST_SETTINGS'
|
||||
}
|
||||
|
||||
interface ReceiveSettingsAction {
|
||||
type: 'RECEIVE_SETTINGS',
|
||||
routes: IRoute[]
|
||||
}
|
||||
|
||||
type KnownAction = RequestSettingsAction | ReceiveSettingsAction;
|
||||
|
||||
export const actionCreators = {
|
||||
requestSettings: (): AppThunkAction<KnownAction> => (dispatch, getState) => {
|
||||
const appState = getState()
|
||||
console.log(appState)
|
||||
|
||||
const routes = [
|
||||
{ path: "/", component: "Home" },
|
||||
{ path: "/counter", component: "Counter" },
|
||||
{ path: "/fetch-data",
|
||||
component: "FetchData",
|
||||
childRoutes: [
|
||||
{
|
||||
path: ":startDateIndex",
|
||||
component: "FetchData"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
dispatch({ type: 'RECEIVE_SETTINGS', routes })
|
||||
|
||||
dispatch({ type: 'REQUEST_SETTINGS' })
|
||||
}
|
||||
}
|
||||
|
||||
const unloadedState: ISettingsState = {
|
||||
routes: [],
|
||||
isLoading: false
|
||||
}
|
||||
|
||||
export const reducer: Reducer<ISettingsState> = (state: ISettingsState | undefined, incomingAction: Action): ISettingsState => {
|
||||
if (state === undefined) {
|
||||
return unloadedState
|
||||
}
|
||||
|
||||
const action = incomingAction as KnownAction
|
||||
switch (action.type) {
|
||||
case 'REQUEST_SETTINGS':
|
||||
return {
|
||||
routes: state.routes,
|
||||
isLoading: true
|
||||
}
|
||||
|
||||
case 'RECEIVE_SETTINGS':
|
||||
return {
|
||||
routes: action.routes,
|
||||
isLoading: false
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
100
clientapp/src/store/reducers/WeatherForecasts.ts
Normal file
100
clientapp/src/store/reducers/WeatherForecasts.ts
Normal file
@ -0,0 +1,100 @@
|
||||
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) {
|
||||
|
||||
const requestParams = {
|
||||
method: 'GET',
|
||||
headers: { accept: 'application/json', 'content-type': 'application/json' }
|
||||
}
|
||||
|
||||
fetch(`https://localhost:7151/WeatherForecast`, requestParams)
|
||||
.then((response) => {
|
||||
const data = response.json() as Promise<WeatherForecast[]>
|
||||
return data
|
||||
})
|
||||
.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
|
||||
}
|
||||
@ -17,7 +17,8 @@
|
||||
"dom"
|
||||
],
|
||||
"skipLibCheck": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"plugins": [{ "name": "typescript-plugin-css-modules" }]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
8875
clientapp/yarn.lock
Normal file
8875
clientapp/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +0,0 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
]
|
||||
}
|
||||
21
scr/ClientApp/.gitignore
vendored
21
scr/ClientApp/.gitignore
vendored
@ -1,21 +0,0 @@
|
||||
# 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*
|
||||
File diff suppressed because it is too large
Load Diff
18318
scr/ClientApp/package-lock.json
generated
18318
scr/ClientApp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "xxxx",
|
||||
"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": "^2.1.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",
|
||||
"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",
|
||||
"cross-env": "6.0.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"nan": "^2.14.1",
|
||||
"react-scripts": "^4.0.3",
|
||||
"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"
|
||||
},
|
||||
"resolutions": {
|
||||
"url-parse": ">=1.5.0",
|
||||
"lodash": ">=4.17.21"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
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'));
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@ -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); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Container } from 'reactstrap';
|
||||
import NavMenu from './NavMenu';
|
||||
|
||||
export default class Layout extends React.PureComponent<{}, { children?: React.ReactNode }> {
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NavMenu />
|
||||
<Container>
|
||||
{this.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="/">xxxx</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,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,27 +0,0 @@
|
||||
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();
|
||||
@ -1,105 +0,0 @@
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
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; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
if (windowIfDefined && windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__) {
|
||||
enhancers.push(windowIfDefined.__REDUX_DEVTOOLS_EXTENSION__());
|
||||
}
|
||||
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
compose(applyMiddleware(...middleware), ...enhancers)
|
||||
);
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace xxxx.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
var rng = new Random();
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateTime.Now.AddDays(index),
|
||||
TemperatureC = rng.Next(-20, 55),
|
||||
Summary = Summaries[rng.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
@page
|
||||
@model ErrorModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace xxxx.Pages
|
||||
{
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
private readonly ILogger<ErrorModel> logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> _logger)
|
||||
{
|
||||
logger = _logger;
|
||||
}
|
||||
public string RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
@using xxxx
|
||||
@namespace xxxx.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace xxxx
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace xxxx
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllersWithViews();
|
||||
|
||||
// In production, the React files will be served from this directory
|
||||
services.AddSpaStaticFiles(configuration =>
|
||||
{
|
||||
configuration.RootPath = "ClientApp/build";
|
||||
});
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseSpaStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllerRoute(
|
||||
name: "default",
|
||||
pattern: "{controller}/{action=Index}/{id?}");
|
||||
});
|
||||
|
||||
app.UseSpa(spa =>
|
||||
{
|
||||
spa.Options.SourcePath = "ClientApp";
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
spa.UseReactDevelopmentServer(npmScript: "start");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace xxxx
|
||||
{
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string Summary { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SpaRoot>ClientApp\</SpaRoot>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.13" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||
<Content Remove="$(SpaRoot)**" />
|
||||
<None Remove="$(SpaRoot)**" />
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||
<!-- Ensure Node.js is installed -->
|
||||
<Exec Command="node --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
||||
</Exec>
|
||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
||||
<Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />
|
||||
|
||||
<!-- Include the newly-built files in the publish output -->
|
||||
<ItemGroup>
|
||||
<DistFiles Include="$(SpaRoot)build\**; $(SpaRoot)build-ssr\**" />
|
||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||
<RelativePath>%(DistFiles.Identity)</RelativePath>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
25
webapi/WeatherForecast.sln
Normal file
25
webapi/WeatherForecast.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31912.275
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeatherForecast", "WeatherForecast\WeatherForecast.csproj", "{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E2805D02-2425-424C-921D-D97341B76F73}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace WeatherForecast.Controllers;
|
||||
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet(Name = "GetWeatherForecast")]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateTime.Now.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
41
webapi/WeatherForecast/Program.cs
Normal file
41
webapi/WeatherForecast/Program.cs
Normal file
@ -0,0 +1,41 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy(name: MyAllowSpecificOrigins,
|
||||
builder => {
|
||||
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
app.UseCors(MyAllowSpecificOrigins);
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
@ -1,24 +1,28 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:57382",
|
||||
"sslPort": 44367
|
||||
"applicationUrl": "http://localhost:55971",
|
||||
"sslPort": 44329
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"WeatherForecast": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7151;http://localhost:5133",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"xxxx": {
|
||||
"commandName": "Project",
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
12
webapi/WeatherForecast/WeatherForecast.cs
Normal file
12
webapi/WeatherForecast/WeatherForecast.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace WeatherForecast;
|
||||
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
14
webapi/WeatherForecast/WeatherForecast.csproj
Normal file
14
webapi/WeatherForecast/WeatherForecast.csproj
Normal file
@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>WeatherForecast</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
webapi/WeatherForecast/appsettings.Development.json
Normal file
8
webapi/WeatherForecast/appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,7 @@
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
Loading…
Reference in New Issue
Block a user