Merge pull request #1178 from agtenr/dev

Thanks @agtenr this is a very timely sample and it looks great.

Would you be interested in presenting this sample at an upcoming SPFx community call? We have an opening for April 9th if you're interested.
This commit is contained in:
Hugo Bernier 2020-04-02 15:30:04 -04:00 committed by GitHub
commit c0ad20c740
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 19255 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

32
samples/react-covid19-info/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.10.0",
"libraryName": "spfx-covid-19-info",
"libraryId": "8e6e959a-f902-4bb8-b1ff-ad7e45126607",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Robin Agten
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,77 @@
# COVID 19 information web part
## Summary
This web part displays info about the COVID-19 virus for a given country.
The following info is displayed:
- Confirmed cases
- Deaths
- Recoverd
![COVID-19 info](./assets/covid-counter.gif)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Solution
Solution|Author(s)
--------|---------
react-covid19-info | [Robin Agten](https://twitter.com/AgtenRobin)
## Web part properties
| Property | Group | Description | Default |
|-------------------------------------- |------------------------ |----------------------------------------------------------------------------------------------------------- |--------- |
| iso2 Country Code | Country Settings | Defines the country for which the COVID-19 info should be displayed example: BE for Belgium | None |
| Show history button | Web part configuration | Determines whether or not the history icon is shown. This can be used to show an graph of historical data | False |
| View more statistics | Web part configuration | Provide an optional external link to more details statistics | None |
| Up count duration | Web part configuration | Number of seconds for the counters to count up | 2 |
| Color for the Confirmed Cases number | Web part configuration | Defines the color of the Confirmed cases number | #69797e |
| Color for the Deaths number | Web part configuration | Defines the color of the Deaths number | #d13438 |
| Color for the Recovered number | Web part configuration | Defines the color of the Recovered number | #498205 |
## Version history
Version|Date|Comments
-------|----|--------
1.0|March 25, 2020|Initial Release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
## Features
* Configurable Country
* Configurable Colors
* Optional historical graph
* Optional 'view more statistics' link
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Using external APIs using httpClient
* [Office Fabric UI REact](https://developer.microsoft.com/en-us/fabric#/)
* [SPFx Controls React](https://sharepoint.github.io/sp-dev-fx-controls-react/)
* [SPFx Property Controls](https://sharepoint.github.io/sp-dev-fx-property-controls/)
* [Recharts](http://recharts.org/en-US/)
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-covid19-info" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"covid-19-info-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/covid19Info/Covid19InfoWebPart.js",
"manifest": "./src/webparts/covid19Info/Covid19InfoWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "spfx-covid-19-info",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "spfx-covid-19-info-client-side-solution",
"id": "8e6e959a-f902-4bb8-b1ff-ad7e45126607",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/spfx-covid-19-info.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(require('gulp'));

18089
samples/react-covid19-info/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
{
"name": "spfx-covid-19-info",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/spfx-controls-react": "1.16.0",
"@pnp/spfx-property-controls": "1.17.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"chart.js": "^2.9.3",
"chartist": "^0.11.4",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-chartist": "^0.14.3",
"react-chartkick": "^0.4.0",
"react-countup": "^4.3.3",
"react-dom": "16.8.5",
"recharts": "^1.8.5"
},
"resolutions": {
"@types/react": "16.8.8"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.10.0",
"@microsoft/sp-tslint-rules": "1.10.0",
"@microsoft/sp-module-interfaces": "1.10.0",
"@microsoft/sp-webpart-workbench": "1.10.0",
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -0,0 +1,17 @@
export interface ICoronaInfo {
countryregion: string;
countrycode: ICountryCode;
lastupdate: string;
location: ILocation;
confirmed: number;
deaths: number;
recovered: number;
}
export interface ILocation {
lat: number;
lng: number;
}
export interface ICountryCode {
iso2: string;
iso3: string;
}

View File

@ -0,0 +1,13 @@
import { IDictionary } from "./IDictionary";
export interface ICoronaInfoHistory {
countryregion: string;
lastupdate: string;
timeseries: IDictionary<TimeInfo>;
}
export interface TimeInfo {
confirmed: number;
deaths: number;
recovered: number;
}

View File

@ -0,0 +1,3 @@
export interface IDictionary<T> {
[Key: string]: T;
}

View File

@ -0,0 +1,55 @@
import { ICoronaService } from "./ICoronaService";
import { HttpClient } from "@microsoft/sp-http";
import { ICoronaInfo } from "../models/ICoronaInfo";
import { ICoronaInfoHistory } from "../models/ICoronaInfoHistory";
export class CoronaService implements ICoronaService {
private httpClient: HttpClient;
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
public async getCountryInfo(countryCode: string): Promise<ICoronaInfo> {
const response = await this.httpClient.get(
`https://wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai/jhu-edu/latest?iso2=${countryCode}&onlyCountries=true`,
HttpClient.configurations.v1
);
if (!response.ok) {
const error = await response.text();
console.log(error);
throw Error(`Error while fetching the data for country with code '${countryCode}'`);
}
const result: ICoronaInfo[] = await response.json();
if (result.length > 0) {
return result[0];
} else {
return null;
}
}
public async getCountryHistory(countryCode: string): Promise<ICoronaInfoHistory> {
const response = await this.httpClient.get(
`https://wuhan-coronavirus-api.laeyoung.endpoint.ainize.ai/jhu-edu/timeseries?iso2=${countryCode}&onlyCountries=true`,
HttpClient.configurations.v1
);
if (!response.ok) {
const error = await response.text();
console.log(error);
throw Error(`Error while fetching the data for country with code '${countryCode}'`);
}
const result: ICoronaInfoHistory[] = await response.json();
if (result.length > 0) {
return result[0];
} else {
return null;
}
}
}

View File

@ -0,0 +1,7 @@
import { ICoronaInfo } from "../models/ICoronaInfo";
import { ICoronaInfoHistory } from "../models/ICoronaInfoHistory";
export interface ICoronaService {
getCountryInfo(countryCode: string): Promise<ICoronaInfo>;
getCountryHistory(countryCode: string): Promise<ICoronaInfoHistory>;
}

View File

@ -0,0 +1,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "6f91d98b-45ec-4915-a409-0d789259c5c7",
"alias": "Covid19InfoWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "COVID-19 Info" },
"description": { "default": "Display regional COVID-19 information" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "COVID 19 Info"
}
}]
}

View File

@ -0,0 +1,137 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneSlider,
PropertyPaneToggle
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import Covid19Info from './components/Covid19Info';
import { ICovid19InfoProps } from './components/ICovid19InfoProps';
import { PropertyFieldColorPicker, PropertyFieldColorPickerStyle } from '@pnp/spfx-property-controls/lib/PropertyFieldColorPicker';
export interface ICovid19InfoWebPartProps {
countryCode: string;
showHistory: boolean;
viewMoreLink: string;
countUpTime: number;
confirmedColor: string;
deathColor: string;
recoveredColor: string;
}
export default class Covid19InfoWebPart extends BaseClientSideWebPart <ICovid19InfoWebPartProps> {
public render(): void {
const element: React.ReactElement<ICovid19InfoProps> = React.createElement(
Covid19Info,
{
countryCode: this.properties.countryCode,
showHistory: this.properties.showHistory,
viewMoreLink: this.properties.viewMoreLink,
countUpTime: this.properties.countUpTime,
confirmedColor: this.properties.confirmedColor,
deathColor: this.properties.deathColor,
recoveredColor: this.properties.recoveredColor,
displayMode: this.displayMode,
httpClient: this.context.httpClient,
onConfigure: this._onConfigure
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Display regional COVID-19 information"
},
groups: [
{
groupName: "Country Settings",
groupFields: [
PropertyPaneTextField('countryCode', {
label: "iso2 Country Code (e.g. US)"
})
]
},
{
groupName: "Web part configuration",
groupFields: [
PropertyPaneToggle('showHistory', {
label: "Show 'View history' button"
}),
PropertyPaneTextField('viewMoreLink', {
label: "Provide an optional link to view more statistics"
}),
PropertyPaneSlider('countUpTime', {
label: "Number of second for the count up counters",
min: 1,
max: 20,
value: 2,
showValue: true,
step:1
}),
PropertyFieldColorPicker('confirmedColor', {
label: 'Color for the Confirmed Cases number',
selectedColor: this.properties.confirmedColor,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: false,
alphaSliderHidden: true,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Precipitation',
key: 'confirmedColorFieldId',
}),
PropertyFieldColorPicker('deathColor', {
label: 'Color for the Deaths number',
selectedColor: this.properties.deathColor,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: false,
alphaSliderHidden: true,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Precipitation',
key: 'deathColorFieldId',
}),
PropertyFieldColorPicker('recoveredColor', {
label: 'Color for the Recovered number',
selectedColor: this.properties.recoveredColor,
onPropertyChange: this.onPropertyPaneFieldChanged,
properties: this.properties,
disabled: false,
isHidden: false,
alphaSliderHidden: true,
style: PropertyFieldColorPickerStyle.Inline,
iconName: 'Precipitation',
key: 'recoveredColorFieldId',
})
]
}
]
}
]
};
}
private _onConfigure = () => {
this.context.propertyPane.open();
}
}

View File

@ -0,0 +1,77 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.covid19Info {
.refreshIcon {
position: absolute;
right: 0px;
z-index: 1;
}
.historyIcon{
position: absolute;
left: 0px;
z-index: 1;
}
.container {
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
.countryRow{
@include ms-Grid-row;
.country {
@include ms-Grid-col;
@include ms-lg12;
padding: 10px;
text-align: center;
@include ms-font-xl;
.icon {
margin-right: 15px;
}
.text {
text-transform: uppercase;
}
}
}
.detailsRow{
@include ms-Grid-row;
padding: 10px;
.infoColumn {
@include ms-Grid-col;
@include ms-md12;
@include ms-lg4;
text-align: center;
.label {
width: 100%;
@include ms-font-l;
}
.number {
padding: 15px;
@include ms-font-xxl;
}
.confirmed {
color: #69797e;
}
.deaths {
color: #d13438;
}
.recovered {
color: #498205;
}
}
}
.lastUpdatedRow {
@include ms-Grid-row;
.lastUpdated {
@include ms-Grid-col;
@include ms-lg12;
padding: 10px;
text-align: center;
@include ms-font-s;
}
}
}
}

View File

@ -0,0 +1,249 @@
import * as React from 'react';
import styles from './Covid19Info.module.scss';
import { ICovid19InfoProps } from './ICovid19InfoProps';
import { ICovid19InfoState } from './ICovid19InfoState';
import { DisplayMode } from '@microsoft/sp-core-library';
import { Placeholder } from '@pnp/spfx-controls-react/lib/Placeholder';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { IconButton } from 'office-ui-fabric-react/lib/Button';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Modal, IModalStyleProps, IModalStyles } from 'office-ui-fabric-react/lib/Modal';
import { mergeStyleSets, IStyle } from "office-ui-fabric-react/lib/Styling";
import { ICoronaService } from '../../../services/ICoronaService';
import { CoronaService } from '../../../services/CoronaService';
import { HistoryModal } from "./HistoryModal/HistoryModal";
import CountUp from "react-countup";
import { ICoronaInfoHistory } from '../../../models/ICoronaInfoHistory';
import { IStyleFunctionOrObject } from 'office-ui-fabric-react/lib/Utilities';
export default class Covid19Info extends React.Component<ICovid19InfoProps, ICovid19InfoState> {
private coronaService: ICoronaService;
constructor(props: ICovid19InfoProps) {
super(props);
this.coronaService = new CoronaService(props.httpClient);
this.state = {
isLoading: true,
coronaInfo: undefined,
globalError: undefined,
showHistoryModal: false
};
}
public componentDidMount() {
this._loadData();
}
public componentDidUpdate(prevProps: ICovid19InfoProps) {
if (prevProps.countryCode !== this.props.countryCode) {
this._loadData();
}
}
public render(): React.ReactElement<ICovid19InfoProps> {
// No country code specified
if (!this.props.countryCode) {
if (this.props.displayMode === DisplayMode.Read) {
return this._renderNoCountryCode();
} else {
return this._renderPlaceHolder();
}
}
// Is loading
if (this.state.isLoading) {
return this._renderSpinner();
}
// Global Error
if (this.state.globalError) {
return this._renderError();
}
// No data found
if (this.state.coronaInfo === null) {
return this._renderNoData();
}
const confirmedColor: string = this.props.confirmedColor
? this.props.confirmedColor
: "#69797e";
const deathColor: string = this.props.deathColor
? this.props.deathColor
: "#d13438";
const recoveredColor: string = this.props.recoveredColor
? this.props.recoveredColor
: "#498205";
return (
<div className={styles.covid19Info}>
<div className={styles.container}>
{this.props.showHistory && (
<IconButton
className={styles.historyIcon}
iconProps={{iconName: "Chart"}}
label={"View history"}
onClick={() => this._showHistoryModal()}
/>
)}
<IconButton
className={styles.refreshIcon}
iconProps={{iconName: "Refresh"}}
label={"Refresh data"}
onClick={() => this._loadData()}
/>
<div className={styles.countryRow}>
<div className={styles.country}>
<Icon className={styles.icon} iconName={"Globe"}/>
<span className={styles.text}>{this.state.coronaInfo.countryregion}</span>
</div>
</div>
<div className={styles.detailsRow}>
<div className={styles.infoColumn}>
<div className={styles.label}>{"Confirmed Cases"}</div>
<div className={styles.number} style={{ color: confirmedColor}}>
<CountUp
end={this.state.coronaInfo.confirmed}
duration={this.props.countUpTime}
/>
</div>
</div>
<div className={styles.infoColumn}>
<div className={styles.label}>{"Deaths"}</div>
<div className={styles.number} style={{ color: deathColor}}>
<CountUp
end={this.state.coronaInfo.deaths}
duration={this.props.countUpTime}
/>
</div>
</div>
<div className={styles.infoColumn}>
<div className={styles.label}>{"Recovered"}</div>
<div className={styles.number} style={{ color: recoveredColor}}>
<CountUp
end={this.state.coronaInfo.recovered}
duration={this.props.countUpTime}
/>
</div>
</div>
</div>
<div className={styles.lastUpdatedRow}>
<div className={styles.lastUpdated}>
{`Last updated:
${new Date(this.state.coronaInfo.lastupdate).toLocaleDateString()}
${new Date(this.state.coronaInfo.lastupdate).toLocaleTimeString()}`
}
{this.props.viewMoreLink &&
<span> - <Link target={"_blank"} href={this.props.viewMoreLink}>View more statistics</Link></span>
}
</div>
</div>
<Modal
isOpen={this.state.showHistoryModal}
onDismiss={this._closeHistoryModal}
isBlocking={false}
styles={this._getModalStyles()}
>
<HistoryModal
countryCode={this.props.countryCode}
confirmedColor={confirmedColor}
deathColor={deathColor}
recoveredColor={recoveredColor}
_loadHistoryData={this._loadHistoryData}
/>
</Modal>
</div>
</div>
);
}
private _loadData = async (): Promise<void> => {
this.setState({isLoading: true});
try {
const coronaInfo = await this.coronaService.getCountryInfo(this.props.countryCode);
this.setState({
isLoading: false,
coronaInfo
});
} catch (error) {
this.setState({
coronaInfo: undefined,
isLoading: false,
globalError: (error as Error).message
});
}
}
private _loadHistoryData = async (): Promise<ICoronaInfoHistory> => {
return await this.coronaService.getCountryHistory(this.props.countryCode);
}
private _showHistoryModal = (): void => {
this.setState({
showHistoryModal: true
});
}
private _closeHistoryModal = (): void => {
this.setState({
showHistoryModal: false
});
}
private _renderNoCountryCode = (): JSX.Element => {
return (
<MessageBar messageBarType={MessageBarType.warning}>
{"Please provide a country code in the web part properties!"}
</MessageBar>
);
}
private _renderPlaceHolder = (): JSX.Element => {
return (
<Placeholder
iconName='Edit'
iconText='Configure your web part'
description='Please provide a country code in the web part properties'
buttonLabel='Configure'
onConfigure={this.props.onConfigure} />
);
}
private _renderSpinner = () => {
return (
<Spinner
label={"Loading COVID-19 data...."}
size={SpinnerSize.medium}
/>
);
}
private _renderError = (): JSX.Element => {
return (
<MessageBar messageBarType={MessageBarType.error}>
{this.state.globalError}
</MessageBar>
);
}
private _renderNoData = (): JSX.Element => {
return (
<MessageBar messageBarType={MessageBarType.info}>
{`No COVID-19 data could be found for country code: '${this.props.countryCode}'`}
</MessageBar>
);
}
private _getModalStyles = (): IStyleFunctionOrObject<IModalStyleProps, IModalStyles> => {
const modalStyles: IStyleFunctionOrObject<IModalStyleProps, IModalStyles> = {
main: {
width: "750px",
height: "500px",
padding: "15px"
}
};
return modalStyles;
}
}

View File

@ -0,0 +1,22 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.historyModalContainer {
.countryRow{
.country {
padding: 10px;
text-align: center;
@include ms-font-xl;
.icon {
margin-right: 15px;
}
.text {
text-transform: uppercase;
}
}
}
.pivotChart{
margin-top: 20px;
}
}

View File

@ -0,0 +1,164 @@
import * as React from 'react';
import styles from './HistoryModal.module.scss';
import { IHistoryModalProps } from "./IHistoryModalProps";
import { IHistoryModalState } from "./IHistoryModalState";
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar';
import { ICoronaInfoHistory } from '../../../../models/ICoronaInfoHistory';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot';
import { IDataPoint } from "./IDataPoint";
import { Icon } from 'office-ui-fabric-react/lib/Icon';
export class HistoryModal extends React.Component<IHistoryModalProps, IHistoryModalState> {
constructor(props: IHistoryModalProps) {
super(props);
this.state = {
isLoading: true,
globalError: undefined,
historyData: undefined
};
}
public componentDidMount() {
this._loadData();
}
public render(): React.ReactElement<IHistoryModalProps> {
// Is loading
if (this.state.isLoading) {
return this._renderSpinner();
}
// Global Error
if (this.state.globalError) {
return this._renderError();
}
// No data found
if (this.state.historyData === null) {
return this._renderNoData();
}
const mappedData = this._mapHistoryData(this.state.historyData);
return (
<div className={styles.historyModalContainer}>
<div className={styles.countryRow}>
<div className={styles.country}>
<Icon className={styles.icon} iconName={"Globe"}/>
<span className={styles.text}>{this.state.historyData.countryregion}</span>
</div>
</div>
<Pivot>
<PivotItem className={styles.pivotChart} headerText={"Confirmed cases"}>
{this._renderChart(mappedData, "confirmed", this.props.confirmedColor, false)}
</PivotItem>
<PivotItem headerText={"Deaths"}>
{this._renderChart(mappedData, "deaths", this.props.deathColor, false)}
</PivotItem>
<PivotItem headerText={"Recovered"}>
{this._renderChart(mappedData, "recovered", this.props.recoveredColor, false)}
</PivotItem>
<PivotItem headerText={"All"}>
{this._renderChart(mappedData, null, null, true)}
</PivotItem>
</Pivot>
</div>
);
}
private _loadData = async (): Promise<void> => {
this.setState({isLoading: true});
try {
const historyData: ICoronaInfoHistory = await this.props._loadHistoryData();
this.setState({
isLoading: false,
historyData
});
} catch (error) {
this.setState({
historyData: undefined,
isLoading: false,
globalError: (error as Error).message
});
}
}
private _mapHistoryData = (historyData: ICoronaInfoHistory): IDataPoint[] => {
const dataPoints: IDataPoint[] = new Array();
for(let key in historyData.timeseries) {
dataPoints.push({
date: key,
confirmed: historyData.timeseries[key].confirmed,
deaths: historyData.timeseries[key].deaths,
recovered: historyData.timeseries[key].recovered
});
}
return dataPoints;
}
private _renderSpinner = () => {
return (
<Spinner
label={"Loading COVID-19 data...."}
size={SpinnerSize.medium}
/>
);
}
private _renderError = (): JSX.Element => {
return (
<MessageBar messageBarType={MessageBarType.error}>
{this.state.globalError}
</MessageBar>
);
}
private _renderNoData = (): JSX.Element => {
return (
<MessageBar messageBarType={MessageBarType.info}>
{`No historical data could be found for country code: '${this.props.countryCode}'`}
</MessageBar>
);
}
private _renderChart = (data: IDataPoint[], dataKey: string, color: string, showAll: boolean): JSX.Element => {
if (showAll) {
return (
<LineChart
width={700}
height={300}
data={data}
margin={{
top: 5, right: 30, left: 20, bottom: 5,
}}
>
<CartesianGrid />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Line type={"monotone"} dataKey={"confirmed"} stroke={this.props.confirmedColor} />
<Line type={"monotone"} dataKey={"deaths"} stroke={this.props.deathColor} />
<Line type={"monotone"} dataKey={"recovered"} stroke={this.props.recoveredColor} />
</LineChart>
);
}
return (
<LineChart
width={700}
height={300}
data={data}
>
<CartesianGrid />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Line type={"monotone"} dataKey={dataKey} stroke={color} />
</LineChart>
);
}
}

View File

@ -0,0 +1,6 @@
export interface IDataPoint {
date: string;
confirmed: number;
deaths: number;
recovered: number;
}

View File

@ -0,0 +1,9 @@
import { ICoronaInfoHistory } from "../../../../models/ICoronaInfoHistory";
export interface IHistoryModalProps {
countryCode: string;
confirmedColor: string;
deathColor: string;
recoveredColor: string;
_loadHistoryData(): Promise<ICoronaInfoHistory>;
}

View File

@ -0,0 +1,7 @@
import { ICoronaInfoHistory } from "../../../../models/ICoronaInfoHistory";
export interface IHistoryModalState {
isLoading: boolean;
historyData: ICoronaInfoHistory;
globalError: string;
}

View File

@ -0,0 +1,15 @@
import { DisplayMode } from "@microsoft/sp-core-library";
import { HttpClient } from "@microsoft/sp-http";
export interface ICovid19InfoProps {
countryCode: string;
showHistory: boolean;
viewMoreLink: string;
countUpTime: number;
confirmedColor: string;
deathColor: string;
recoveredColor: string;
displayMode: DisplayMode;
httpClient: HttpClient;
onConfigure(): void;
}

View File

@ -0,0 +1,8 @@
import { ICoronaInfo } from "../../../models/ICoronaInfo";
export interface ICovid19InfoState {
isLoading: boolean;
coronaInfo: ICoronaInfo;
globalError: string;
showHistoryModal: boolean;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}