Merge pull request #1 from agtenr/react-covid19-info
React covid19 info
This commit is contained in:
commit
b86c9f39f4
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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.
|
|
@ -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 |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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'));
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface IDictionary<T> {
|
||||
[Key: string]: T;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IDataPoint {
|
||||
date: string;
|
||||
confirmed: number;
|
||||
deaths: number;
|
||||
recovered: number;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ICoronaInfoHistory } from "../../../../models/ICoronaInfoHistory";
|
||||
|
||||
export interface IHistoryModalProps {
|
||||
countryCode: string;
|
||||
confirmedColor: string;
|
||||
deathColor: string;
|
||||
recoveredColor: string;
|
||||
_loadHistoryData(): Promise<ICoronaInfoHistory>;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { ICoronaInfoHistory } from "../../../../models/ICoronaInfoHistory";
|
||||
|
||||
export interface IHistoryModalState {
|
||||
isLoading: boolean;
|
||||
historyData: ICoronaInfoHistory;
|
||||
globalError: string;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue