Added sample react-google-fit (#755)
|
@ -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,11 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.7.1",
|
||||
"libraryName": "react-google-fit",
|
||||
"libraryId": "2469a81b-8fb7-4cb4-a710-50f324465560",
|
||||
"packageManager": "npm",
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
## Web part displaying Google Fit information
|
||||
|
||||
### Summary
|
||||
This sample demonstrates integration of Google Fit information with SharePoint Framework. The Google Fitness REST APIs allows developers to extend it further and create their own dashboards. Google Fitness REST APIs are useful if you have fitness app and you want to integrate your data with google fit or if you just want to collect Fitness data and display some information to the users. This web part helps to display the key fitness information (activity time spent, distance travelled, calories burned, step count) from the Google fit data source.
|
||||
|
||||
![Web part preview][figure1]
|
||||
|
||||
### Generate OAuth 2.0 client ID
|
||||
In order to use Google REST APIs, we need to generate an OAuth 2.0 client ID. Follow below procedure to generate it.
|
||||
1. Open Google API Console from [here](https://console.developers.google.com/flows/enableapi?apiid=fitness).
|
||||
2. Select any existing project or choose to create a new project.
|
||||
3. Click Continue. <br/>
|
||||
![Create a new project][figure3]
|
||||
4. Once the project is created, we will have to generate the credentials.
|
||||
5. Click "Go to credentials".<br/>
|
||||
![Generate credentials][figure4]
|
||||
6. Select the options as highlighted below:<br/>
|
||||
![Add credentials to your project][figure5]
|
||||
7. Click "What credentials do I need?".
|
||||
8. Under Authorized JavaScript origins, add SharePoint Online site url (e.g. https://contoso.sharepoint.com) or https://localhost:4321, if you are using SharePoint local workbench.
|
||||
9. Under Authorized redirect URI, add https://localhost:4321/auth/google/callback, if you are using SharePoint local workbench.<br/>
|
||||
![Add authorized origins][figure6]
|
||||
10. Click "Create OAuth client ID".
|
||||
11. Set up the OAuth 2.0 consent screen.<br/>
|
||||
![Setup OAuth consent][figure7]
|
||||
12. Click Continue.
|
||||
13. The Client id will be generated. Note it down to use in web part property.<br/>
|
||||
![OAuth ClientId][figure8]
|
||||
14. Click Done.
|
||||
|
||||
### Configure the Web Part to use
|
||||
1. Add "Google Fit Activity Viewer" web part on SharePoint page.
|
||||
2. Edit the web part.
|
||||
3. Add above generated OAuth 2.0 client ID to "ClientId Field" web part property.
|
||||
4. Save the changes.
|
||||
![SharePoint Run][figure2]
|
||||
|
||||
### NPM Packages Used
|
||||
Below NPM package is used to develop this sample.
|
||||
1. react-google-authorize (https://www.npmjs.com/package/react-google-authorize)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.7.1-green.svg)
|
||||
|
||||
## Applies to
|
||||
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-google-fit|[Nanddeep Nachan](https://www.linkedin.com/in/nanddeepnachan/) (SharePoint Consultant, [@NanddeepNachan](https://twitter.com/NanddeepNachan))
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|January 14, 2019|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.**
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- SharePoint Online tenant
|
||||
- Site Collection created under the **/sites/** or **/**
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repo
|
||||
- npm i
|
||||
- gulp serve --nobrowser
|
||||
- Open workbench on your tennant, i.e. https://contoso.sharepoint.com/sites/salesteam/_layouts/15/workbench.aspx
|
||||
- Search and add web part "Google Fit Activity Viewer"
|
||||
|
||||
## Features
|
||||
This sample web part shows how adaptive cards can be used effectively with SharePoint Framework to render an image gallery with data stored in a SharePoint list.
|
||||
- Integrating Google Fit information
|
||||
- Consuming Google Fit REST APIs
|
||||
- Creating extensible services
|
||||
- Using @react-google-authorize
|
||||
|
||||
|
||||
[figure1]: ./assets/webpart-preview.png
|
||||
[figure2]: ./assets/sharepoint-run.gif
|
||||
[figure3]: ./assets/create-new-project.png
|
||||
[figure4]: ./assets/generate-credentials.png
|
||||
[figure5]: ./assets/add-credentials-to-your-project.png
|
||||
[figure6]: ./assets/add-authorized-origins.png
|
||||
[figure7]: ./assets/setup-oauth-consent.png
|
||||
[figure8]: ./assets/oauth-clientid.png
|
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 320 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"google-fit-activity-viewer-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/googleFitActivityViewer/GoogleFitActivityViewerWebPart.js",
|
||||
"manifest": "./src/webparts/googleFitActivityViewer/GoogleFitActivityViewerWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"GoogleFitActivityViewerWebPartStrings": "lib/webparts/googleFitActivityViewer/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": "react-google-fit",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-google-fit-client-side-solution",
|
||||
"id": "2469a81b-8fb7-4cb4-a710-50f324465560",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-google-fit.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 gulp = require('gulp');
|
||||
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(gulp);
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "react-google-fit",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.7.1",
|
||||
"@microsoft/sp-lodash-subset": "1.7.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.7.1",
|
||||
"@microsoft/sp-webpart-base": "1.7.1",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.4.2",
|
||||
"@types/react-dom": "16.0.5",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"react": "16.3.2",
|
||||
"react-dom": "16.3.2",
|
||||
"react-google-authorize": "^1.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.7.1",
|
||||
"@microsoft/sp-tslint-rules": "1.7.1",
|
||||
"@microsoft/sp-module-interfaces": "1.7.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.7.1",
|
||||
"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,130 @@
|
|||
import { ServiceScope, ServiceKey } from "@microsoft/sp-core-library";
|
||||
import { IDataService } from './IDataService';
|
||||
import { HttpClient, HttpClientResponse, IHttpClientOptions } from '@microsoft/sp-http';
|
||||
import { PageContext } from '@microsoft/sp-page-context';
|
||||
import { IFitnessActivity, IFitnessPoint, IFitnessPointValue } from './IFitnessActivity';
|
||||
|
||||
export class GoogleFitService implements IDataService {
|
||||
public static readonly serviceKey: ServiceKey<IDataService> = ServiceKey.create<IDataService>('googleFit:data-service', GoogleFitService);
|
||||
private _httpClient: HttpClient;
|
||||
private _pageContext: PageContext;
|
||||
|
||||
constructor(serviceScope: ServiceScope) {
|
||||
serviceScope.whenFinished(() => {
|
||||
// Configure the required dependencies
|
||||
this._httpClient = serviceScope.consume(HttpClient.serviceKey);
|
||||
this._pageContext = serviceScope.consume(PageContext.serviceKey);
|
||||
});
|
||||
}
|
||||
|
||||
// Get step count from Google fit data source
|
||||
public getStepCount(accessToken: string): Promise<number> {
|
||||
return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {
|
||||
this.getGoogleFitData('derived:com.google.step_count.delta:com.google.android.gms:estimated_steps', accessToken)
|
||||
.then((fitnessData: IFitnessActivity): void => {
|
||||
var stepsCount: number = 0;
|
||||
var i: number = 0;
|
||||
var j: number = 0;
|
||||
|
||||
// Calculate step count of each activity
|
||||
for (i = 0; i < fitnessData.point.length; i++) {
|
||||
for (j = 0; j < fitnessData.point[i].value.length; j++) {
|
||||
stepsCount += fitnessData.point[i].value[j].intVal;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(stepsCount);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get calories burned from Google fit data source
|
||||
public getCalories(accessToken: string): Promise<number> {
|
||||
return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {
|
||||
this.getGoogleFitData('derived:com.google.calories.expended:com.google.android.gms:merge_calories_expended', accessToken)
|
||||
.then((fitnessData: IFitnessActivity): void => {
|
||||
var calories: number = 0;
|
||||
var i: number = 0;
|
||||
var j: number = 0;
|
||||
|
||||
// Calculate calories burned during each activity
|
||||
for (i = 0; i < fitnessData.point.length; i++) {
|
||||
for (j = 0; j < fitnessData.point[i].value.length; j++) {
|
||||
calories += fitnessData.point[i].value[j].fpVal;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(calories);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get distance travelled from Google fit data source
|
||||
public getDistance(accessToken: string): Promise<number> {
|
||||
return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {
|
||||
this.getGoogleFitData('derived:com.google.distance.delta:com.google.android.gms:merge_distance_delta', accessToken)
|
||||
.then((fitnessData: IFitnessActivity): void => {
|
||||
var distance: number = 0;
|
||||
var i: number = 0;
|
||||
var j: number = 0;
|
||||
|
||||
// Calculate distance travelled during each activity
|
||||
for (i = 0; i < fitnessData.point.length; i++) {
|
||||
for (j = 0; j < fitnessData.point[i].value.length; j++) {
|
||||
distance += fitnessData.point[i].value[j].fpVal;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(distance / 1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get activity time from Google fit data source
|
||||
public getActivityTime(accessToken: string): Promise<number> {
|
||||
return new Promise<number>((resolve: (itemId: number) => void, reject: (error: any) => void): void => {
|
||||
this.getGoogleFitData('derived:com.google.activity.segment:com.google.android.gms:merge_activity_segments', accessToken)
|
||||
.then((fitnessData: IFitnessActivity): void => {
|
||||
var activityTime: number = 0;
|
||||
var i: number = 0;
|
||||
var j: number = 0;
|
||||
|
||||
// Calculate activity time spent for each activity
|
||||
for (i = 0; i < fitnessData.point.length; i++) {
|
||||
for (j = 0; j < fitnessData.point[i].value.length; j++) {
|
||||
activityTime += fitnessData.point[i].value[j].intVal;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(activityTime);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get Google fit data by calling the REST API
|
||||
private getGoogleFitData(activityScope: string, accessToken: string): Promise<IFitnessActivity> {
|
||||
// Calculate start date, end date
|
||||
var startTime: number = new Date().getTime();
|
||||
var todayMidnight: Date = new Date();
|
||||
todayMidnight.setHours(0, 0, 0, 0);
|
||||
var endTime: number = todayMidnight.getTime();
|
||||
|
||||
const requestHeaders: Headers = new Headers();
|
||||
requestHeaders.append("Content-type", "application/json");
|
||||
requestHeaders.append("Cache-Control", "no-cache");
|
||||
|
||||
const postOptions: IHttpClientOptions = {
|
||||
headers: requestHeaders
|
||||
};
|
||||
|
||||
let sessionUrl: string = `https://www.googleapis.com/fitness/v1/users/me/dataSources/` + activityScope + `/datasets/` + startTime + `000000-` + endTime + `000000?access_token=` + accessToken;
|
||||
return new Promise<IFitnessActivity>((resolve: (itemId: IFitnessActivity) => void, reject: (error: any) => void): void => {
|
||||
this._httpClient.get(sessionUrl, HttpClient.configurations.v1, postOptions)
|
||||
.then((response: HttpClientResponse) => {
|
||||
response.json().then((responseJSON: IFitnessActivity) => {
|
||||
resolve(responseJSON);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IDataService {
|
||||
getStepCount: (accessToken: string) => Promise<any>;
|
||||
getCalories: (accessToken: string) => Promise<any>;
|
||||
getDistance: (accessToken: string) => Promise<any>;
|
||||
getActivityTime: (accessToken: string) => Promise<any>;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export interface IFitnessActivity {
|
||||
dataSourceId: string;
|
||||
maxEndTimeNs: string;
|
||||
minStartTimeNs: string;
|
||||
point: IFitnessPoint[];
|
||||
}
|
||||
|
||||
export interface IFitnessPoint {
|
||||
dataTypeName: string;
|
||||
endTimeNanos: string;
|
||||
modifiedTimeMillis: string;
|
||||
value: IFitnessPointValue[];
|
||||
}
|
||||
|
||||
export interface IFitnessPointValue {
|
||||
intVal: number;
|
||||
fpVal: number;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "5626ea8d-ef33-451a-9a96-2a5120228e70",
|
||||
"alias": "GoogleFitActivityViewerWebPart",
|
||||
"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,
|
||||
"preconfiguredEntries": [
|
||||
{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": {
|
||||
"default": "Other"
|
||||
},
|
||||
"title": {
|
||||
"default": "Google Fit Activity Viewer"
|
||||
},
|
||||
"description": {
|
||||
"default": "Display Google Fit Activities"
|
||||
},
|
||||
"officeFabricIconFontName": "Health",
|
||||
"properties": {
|
||||
"description": "Google Fit Activity Viewer"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'GoogleFitActivityViewerWebPartStrings';
|
||||
import GoogleFitActivityViewer from './components/GoogleFitActivityViewer';
|
||||
import { IGoogleFitActivityViewerProps } from './components/IGoogleFitActivityViewerProps';
|
||||
|
||||
export interface IGoogleFitActivityViewerWebPartProps {
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export default class GoogleFitActivityViewerWebPart extends BaseClientSideWebPart<IGoogleFitActivityViewerWebPartProps> {
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IGoogleFitActivityViewerProps> = React.createElement(
|
||||
GoogleFitActivityViewer,
|
||||
{
|
||||
clientId: this.properties.clientId,
|
||||
serviceScope: this.context.serviceScope
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('clientId', {
|
||||
label: strings.ClientIdFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.googleFitActivityViewer {
|
||||
.container {
|
||||
max-width: 700px;
|
||||
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);
|
||||
}
|
||||
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
@include ms-fontColor-white;
|
||||
background-color: $ms-color-themeDark;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.column {
|
||||
@include ms-Grid-col;
|
||||
@include ms-lg10;
|
||||
@include ms-xl8;
|
||||
@include ms-xlPush2;
|
||||
@include ms-lgPush1;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include ms-font-xl;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
@include ms-font-l;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include ms-font-l;
|
||||
@include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.button {
|
||||
// Our button
|
||||
text-decoration: none;
|
||||
height: 32px;
|
||||
|
||||
// Primary Button
|
||||
min-width: 80px;
|
||||
background-color: $ms-color-themePrimary;
|
||||
border-color: $ms-color-themePrimary;
|
||||
color: $ms-color-white;
|
||||
|
||||
// Basic Button
|
||||
outline: transparent;
|
||||
position: relative;
|
||||
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: $ms-font-size-m;
|
||||
font-weight: $ms-font-weight-regular;
|
||||
border-width: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
|
||||
.label {
|
||||
font-weight: $ms-font-weight-semibold;
|
||||
font-size: $ms-font-size-m;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin: 0 4px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
// Office UI Fabric (included from: https://raw.githubusercontent.com/OfficeDev/office-ui-fabric-js/master/src/components/Table/Table.scss)
|
||||
// --------------------------------------------------
|
||||
// Data table styles
|
||||
|
||||
.msTable {
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.msTable tr,
|
||||
.msTableRow {
|
||||
display: table-row;
|
||||
line-height: 35px;
|
||||
font-weight: $ms-font-weight-semilight;
|
||||
font-size: $ms-font-size-xl;
|
||||
color: $ms-color-neutralPrimary;
|
||||
}
|
||||
|
||||
.msTableRowHeader {
|
||||
line-height: 35px;
|
||||
font-weight: $ms-font-weight-semibold;
|
||||
font-size: $ms-font-size-xl;
|
||||
color: $ms-color-white;
|
||||
background-color: $ms-color-themeDark;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.msTable th,
|
||||
.msTable td,
|
||||
.msTableCell {
|
||||
display: table-cell;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
// Style the first row as a header.
|
||||
.msTable thead th,
|
||||
.msTableHead {
|
||||
font-weight: $ms-font-weight-semilight;
|
||||
font-size: $ms-font-size-xs;
|
||||
color: $ms-color-neutralSecondary;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
import * as React from 'react';
|
||||
import styles from './GoogleFitActivityViewer.module.scss';
|
||||
import { IGoogleFitActivityViewerProps } from './IGoogleFitActivityViewerProps';
|
||||
import { escape } from '@microsoft/sp-lodash-subset';
|
||||
import { HttpClient, SPHttpClient, HttpClientConfiguration, HttpClientResponse, ODataVersion, IHttpClientConfiguration, IHttpClientOptions, ISPHttpClientOptions } from '@microsoft/sp-http';
|
||||
import { GoogleAuthorize } from 'react-google-authorize';
|
||||
import { IGoogleFitActivityViewerState } from './IGoogleFitActivityViewerState';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { ServiceScope, Environment, EnvironmentType } from '@microsoft/sp-core-library';
|
||||
import { GoogleFitService } from '../../../services/GoogleFitService';
|
||||
import { IDataService } from '../../../services/IDataService';
|
||||
|
||||
export default class GoogleFitActivityViewer extends React.Component<IGoogleFitActivityViewerProps, IGoogleFitActivityViewerState> {
|
||||
private dataCenterServiceInstance: IDataService;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isGoogleAuthenticated: false,
|
||||
accessToken: "",
|
||||
stepCount: 0,
|
||||
calories: 0,
|
||||
distance: 0,
|
||||
activityTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IGoogleFitActivityViewerProps> {
|
||||
const responseGoogle = (response) => {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
isGoogleAuthenticated: true,
|
||||
accessToken: response.access_token
|
||||
};
|
||||
});
|
||||
|
||||
this.readStepCount(this.state.accessToken);
|
||||
this.readCalories(this.state.accessToken);
|
||||
this.readDistance(this.state.accessToken);
|
||||
this.readActivityTime(this.state.accessToken);
|
||||
};
|
||||
|
||||
const formatNumber = (num) => parseFloat(num.toFixed(2)).toLocaleString().replace(/\.([0-9])$/, ".$10");
|
||||
|
||||
return (
|
||||
<div className={styles.googleFitActivityViewer}>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!this.state.isGoogleAuthenticated && this.state.accessToken == "" &&
|
||||
<GoogleAuthorize
|
||||
scope={'https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'}
|
||||
clientId={this.props.clientId}
|
||||
onSuccess={responseGoogle}
|
||||
onFailure={responseGoogle}
|
||||
>
|
||||
<span>Login with Google</span>
|
||||
</GoogleAuthorize>
|
||||
}
|
||||
|
||||
{
|
||||
this.state.isGoogleAuthenticated &&
|
||||
<div>
|
||||
<div className={styles.msTable}>
|
||||
<div className={styles.msTableRowHeader}>
|
||||
<span className={styles.msTableCell}>
|
||||
Today, {new Date().toDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.msTable}>
|
||||
<div className={styles.msTableRow}>
|
||||
<span className={styles.msTableCell}>
|
||||
<Icon iconName="Clock" className="ms-IconExample" />
|
||||
</span>
|
||||
<span className={styles.msTableCell}>
|
||||
<b>{formatNumber(this.state.activityTime)}</b> min
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.msTableRow}>
|
||||
<span className={styles.msTableCell}>
|
||||
<Icon iconName="POI" className="ms-IconExample" />
|
||||
</span>
|
||||
<span className={styles.msTableCell}>
|
||||
<b>{formatNumber(this.state.distance)}</b> km
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.msTableRow}>
|
||||
<span className={styles.msTableCell}>
|
||||
<Icon iconName="CaloriesAdd" className="ms-IconExample" />
|
||||
</span>
|
||||
<span className={styles.msTableCell}>
|
||||
<b>{formatNumber(this.state.calories)}</b> calories
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.msTableRow}>
|
||||
<span className={styles.msTableCell}>
|
||||
<Icon iconName="Running" className="ms-IconExample" />
|
||||
</span>
|
||||
<span className={styles.msTableCell}>
|
||||
<b>{formatNumber(this.state.stepCount)}</b> steps
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private readStepCount(accessToken: string): void {
|
||||
let serviceScope: ServiceScope = this.props.serviceScope;
|
||||
this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);
|
||||
|
||||
this.dataCenterServiceInstance.getStepCount(accessToken).then((stepCount: number) => {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
stepCount: stepCount
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private readCalories(accessToken: string): void {
|
||||
let serviceScope: ServiceScope = this.props.serviceScope;
|
||||
this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);
|
||||
|
||||
this.dataCenterServiceInstance.getCalories(accessToken).then((calories: number) => {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
calories: calories
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private readDistance(accessToken: string): void {
|
||||
let serviceScope: ServiceScope = this.props.serviceScope;
|
||||
this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);
|
||||
|
||||
this.dataCenterServiceInstance.getDistance(accessToken).then((distance: number) => {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
distance: distance
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private readActivityTime(accessToken: string): void {
|
||||
let serviceScope: ServiceScope = this.props.serviceScope;
|
||||
this.dataCenterServiceInstance = serviceScope.consume(GoogleFitService.serviceKey);
|
||||
|
||||
this.dataCenterServiceInstance.getActivityTime(accessToken).then((activityTime: number) => {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
activityTime: activityTime
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { ServiceScope } from '@microsoft/sp-core-library';
|
||||
|
||||
export interface IGoogleFitActivityViewerProps {
|
||||
clientId: string;
|
||||
serviceScope: ServiceScope;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface IGoogleFitActivityViewerState {
|
||||
isGoogleAuthenticated: boolean;
|
||||
accessToken: string;
|
||||
stepCount: number;
|
||||
calories: number;
|
||||
distance: number;
|
||||
activityTime: number;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function () {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"ClientIdFieldLabel": "ClientId Field"
|
||||
}
|
||||
});
|
10
samples/react-google-fit/src/webparts/googleFitActivityViewer/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IGoogleFitActivityViewerWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
ClientIdFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'GoogleFitActivityViewerWebPartStrings' {
|
||||
const strings: IGoogleFitActivityViewerWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"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
|
||||
}
|
||||
}
|