Merge pull request #1437 from kunj-sangani/master

Adding sample to create avatar using reactAvatar
This commit is contained in:
Hugo Bernier 2020-08-12 01:20:11 -04:00 committed by GitHub
commit 1aedb21f19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 18163 additions and 0 deletions

View File

@ -0,0 +1,89 @@
# SPFx Avatar
## Summary
This is a sample web part that helps user create their avatar and save as profile picture. User can even download their avatar as PNG file. This webpart can be useful in Intranet to help user create their avatar and save it as profile picture.
##
![directory](/samples/react-avatar/assets/reactAvatarOutcome.gif)
## Features
* Avatar Generator from multiple options available
* Set Avatar as Profile Picture.
* Download Avatar as PNG file.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Online](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
## Prerequisites
* SharePoint Online tenant
* You have to provide permission in SharePoint admin for accessing Graph API on behalf of your solution. We can do it before deployment as proactive steps, or after deployment. You can refer to [steps about how to do this post-deployment](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aad-tutorial#deploy-the-solution-and-grant-permissions). You have to use API Access Page of SharePoint admin and add below permission for our use case.
```
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite"
}
]
```
## Concepts
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Using react framework in SPFx webpart
* Calling Graph API my Photo to store the image in SPFx webpart
* Usage of Material UI Tab and Dialog
* Usage of [avataaars](https://getavataaars.com/)
## WebPart Properties
Property |Type|Required| comments
--------------------|----|--------|----------
Web Part Title | Text| no|
## Solution
The web part Use avataaars library for creating avatars and MS Graph with User.ReadWrite and User.ReadWriteAll for saving avatar as current users Profile Picture.FileSaver for downloading avatar image as png file.
Solution|Author(s)
--------|---------
react Avatar|Kunj Sangani
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|August 1, 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 build`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- `Add to AppCatalog and deploy`

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 KiB

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"avatar-generator-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/avatarGenerator/AvatarGeneratorWebPart.js",
"manifest": "./src/webparts/avatarGenerator/AvatarGeneratorWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AvatarGeneratorWebPartStrings": "lib/webparts/avatarGenerator/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": "react-avatars",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-avatars-client-side-solution",
"id": "b96dfed7-ec48-4082-ba50-f6c7b09143c7",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false,
"webApiPermissionRequests": [
{
"resource": "Microsoft Graph",
"scope": "User.ReadWrite"
}
]
},
"paths": {
"zippedPackage": "solution/react-avatars.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 -->"
}

7
samples/react-avatar/gulpfile.js vendored Normal file
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'));

17235
samples/react-avatar/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "react-avatars",
"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": {
"@material-ui/core": "^4.11.0",
"@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",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"avataaars": "^1.2.1",
"file-saver": "^2.0.2",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.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,27 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "4052b4ec-72e9-4248-b411-18a4cd438d4b",
"alias": "AvatarGeneratorWebPart",
"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": "AvatarGenerator" },
"description": { "default": "AvatarGenerator for SharePoint Profile Picture" },
"officeFabricIconFontName": "ContactInfo",
"properties": {
"description": "AvatarGenerator"
}
}]
}

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as strings from 'AvatarGeneratorWebPartStrings';
import AvatarGenerator from './components/AvatarGenerator';
import { IAvatarGeneratorProps } from './components/IAvatarGeneratorProps';
export interface IAvatarGeneratorWebPartProps {
description: string;
}
export default class AvatarGeneratorWebPart extends BaseClientSideWebPart <IAvatarGeneratorWebPartProps> {
public render(): void {
const element: React.ReactElement<IAvatarGeneratorProps> = React.createElement(
AvatarGenerator,
{
description: this.properties.description,
context:this.context
}
);
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: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: 'Web Part Title'
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,93 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.avatarGenerator {
.container {
max-width: 1200px;
margin: 0px auto;
}
.row {
@include ms-Grid-row;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
}
.column4 {
@include ms-Grid-col;
@include ms-lg4;
@include ms-xl4;
}
.column8 {
@include ms-Grid-col;
@include ms-lg8;
@include ms-xl8;
}
.piece{
display: inline-block;
}
.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;
}
}
}
:global{
#workbenchPageContent{
max-width: 1200px;
}
.ms-ChoiceField{
margin-left: 40px;
}
}

View File

@ -0,0 +1,422 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import styles from './AvatarGenerator.module.scss';
import { IAvatarGeneratorProps } from './IAvatarGeneratorProps';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import { Avatar, Piece, allOptions, OptionContext, AvatarStyle } from "avataaars";
import { TabPanel } from "./TabPanel";
import * as ReactDOM from 'react-dom';
import Button from '@material-ui/core/Button';
import * as FileSaver from 'file-saver';
import { MSGraphClient } from "@microsoft/sp-http";
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
const options: IChoiceGroupOption[] = [
{ key: AvatarStyle.Circle, text: 'Circle' },
{ key: AvatarStyle.Transparent, text: 'Transparent' }
];
export interface IAvatarGeneratorState {
avatarStyle: AvatarStyle;
value: number;
open: boolean;
savedMessage: string;
savedMessageTitle: string;
}
export default class AvatarGenerator extends React.Component<IAvatarGeneratorProps, IAvatarGeneratorState> {
public static childContextTypes = {
optionContext: PropTypes.instanceOf(OptionContext)
};
private avatarRef: Avatar | null = null;
private canvasRef: HTMLCanvasElement | null = null;
private optionContext: OptionContext = new OptionContext(allOptions);
constructor(props: IAvatarGeneratorProps) {
super(props);
this.state = {
avatarStyle: AvatarStyle.Circle,
value: 0,
open: false,
savedMessage: "",
savedMessageTitle: ""
};
}
public getChildContext() {
return { optionContext: this.optionContext };
}
public componentDidMount() {
const { optionContext } = this;
const value = optionContext.options;
optionContext.options.map((option, index) => {
const optionState = optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
}
});
this.forceUpdate();
}
public a11yProps(index) {
return {
id: `scrollable-auto-tab-${index}`,
'aria-controls': `scrollable-auto-tabpanel-${index}`,
};
}
private triggerDownload = (imageBlob: Blob, fileName: string) => {
FileSaver.saveAs(imageBlob, fileName);
}
private saveAsProfile = (imageBlob: Blob) => {
var reader = new FileReader();
var tempfile = new File([imageBlob], "myavataaars.png", { type: "image/png" });
this.props.context.msGraphClientFactory
.getClient().then((client: MSGraphClient) => {
client
.api("me/photo/$value")
.version("v1.0").header("Content-Type", "image/png").put(tempfile, (err, res) => {
if (!err) {
this.setState({
open: true, savedMessage: "Your profile picture is updated",
savedMessageTitle: "Profile Picture Saved"
});
} else {
this.setState({
open: true, savedMessage: err.message,
savedMessageTitle: "Error while saving Profile Picture"
});
}
});
});
}
private onDownloadPNG = (isDownload: boolean) => {
const svgNode = ReactDOM.findDOMNode(this.avatarRef!);
const canvas = this.canvasRef!;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const anyWindow = window as any;
const DOMURL = anyWindow.URL || anyWindow.webkitURL || window;
const data = svgNode["outerHTML"];
const img = new Image(canvas.width, canvas.height);
const svg = new Blob([data], { type: 'image/svg+xml' });
const url = DOMURL.createObjectURL(svg);
img.onload = () => {
ctx.save();
ctx.scale(2, 2);
ctx.drawImage(img, 0, 0);
ctx.restore();
DOMURL.revokeObjectURL(url);
this.canvasRef!.toBlob(imageBlob => {
if (isDownload) {
this.triggerDownload(imageBlob!, 'myavataaars.png');
} else {
this.saveAsProfile(imageBlob!);
}
});
};
img.src = url;
}
public _onChange = (ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void => {
this.setState({ avatarStyle: option.key as AvatarStyle });
}
public handleClose = () => {
this.setState({ open: false });
}
public render(): React.ReactElement<IAvatarGeneratorProps> {
let count: number = -1;
let internalcount: number = -1;
return (
<div className={styles.avatarGenerator}>
{this.props.description &&
<h1>{this.props.description}</h1>}
<div className={styles.container}>
<div className={styles.row} style={{ textAlign: "center" }}>
<div className={styles.column4}>
<Avatar ref={(ref) => { this.avatarRef = ref; }}
avatarStyle={this.state.avatarStyle}
/>
<ChoiceGroup styles={{ flexContainer: { display: "flex" } }}
defaultSelectedKey={AvatarStyle.Circle} options={options} onChange={this._onChange} label="Avatar Style" />
<Button variant="contained" style={{ marginTop: 10 }} color="primary" onClick={(ev) => {
this.onDownloadPNG(false);
}}>
Save as Profile Picture
</Button>
<Button variant="contained" style={{ marginTop: 10 }} color="primary" onClick={(ev) => {
this.onDownloadPNG(true);
}}>
Download as Image
</Button>
</div>
<div className={styles.column8}>
<AppBar position="static">
<Tabs
variant="scrollable"
scrollButtons="on"
indicatorColor="primary"
value={this.state.value}
onChange={(ev, num) => { this.setState({ value: num }); }}
aria-label="simple tabs example">
{this.optionContext.options.map((option, index) => {
const optionState = this.optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
} else {
count++;
return <Tab label={option.label} {...this.a11yProps(count)}></Tab>;
}
})}
</Tabs>
</AppBar>
{this.optionContext.options.map((option, index) => {
const optionState = this.optionContext.getOptionState(option.key)!;
if (optionState.available <= 0) {
return null;
} else {
internalcount++;
switch (option.key) {
case "topType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle="Circle"
pieceType="top"
pieceSize="100"
topType={type} /></div>);
})}
</TabPanel>;
case "accessoriesType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="accessories"
pieceSize="100"
accessoriesType={type} /></div>);
})}
</TabPanel>;
case "hairColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
> <Piece
avatarStyle=""
pieceType="top"
pieceSize="100"
topType="LongHairFro"
hairColor={type} />
</div>);
})}
</TabPanel>;
break;
case "facialHairType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="facialHair"
pieceSize="100"
facialHairType={type} /></div>);
})}
</TabPanel>;
break;
case "clotheType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="clothe"
pieceSize="100"
clotheType={type} /></div>);
})}
</TabPanel>;
break;
case "clotheColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="clotheColor"
pieceSize="100"
clotheColor={type} /></div>);
})}
</TabPanel>;
break;
case "graphicType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="graphics"
pieceSize="100"
graphicType={type} /></div>);
})}
</TabPanel>;
break;
case "eyeType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="eyes"
pieceSize="100"
eyeType={type} /></div>);
})}
</TabPanel>;
break;
case "eyebrowType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="eyebrows"
pieceSize="100"
eyebrowType={type} /></div>);
})}
</TabPanel>;
break;
case "mouthType":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="mouth"
pieceSize="100"
mouthType={type} /></div>);
})}
</TabPanel>;
break;
case "skinColor":
return <TabPanel value={this.state.value} index={internalcount}>
{optionState.options
.map(type => {
return (<div className={styles.piece}
onClick={(ev) => {
var selectedData = this.optionContext["_data"];
selectedData[`${option.key}`] = type;
this.optionContext.setData(selectedData as any);
}}
><Piece avatarStyle=""
pieceType="skin"
pieceSize="100"
skinColor={type} /></div>);
})}
</TabPanel>;
break;
default:
return null;
break;
}
}
})}
</div>
</div>
<canvas
style={{ display: "none" }}
width='528'
height='560'
ref={(ref) => { this.canvasRef = ref; }}
/>
</div>
<Dialog
open={this.state.open}
onClose={this.handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{this.state.savedMessageTitle}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{this.state.savedMessage}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

View File

@ -0,0 +1,6 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
export interface IAvatarGeneratorProps {
description: string;
context:WebPartContext;
}

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import Box from '@material-ui/core/Box';
export interface ITabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
export const TabPanel: React.FunctionComponent<ITabPanelProps> = (props: ITabPanelProps) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`scrollable-auto-tabpanel-${index}`}
aria-labelledby={`scrollable-auto-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<div>{children}</div>
</Box>
)}
</div>
);
};

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IAvatarGeneratorWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'AvatarGeneratorWebPartStrings' {
const strings: IAvatarGeneratorWebPartStrings;
export = strings;
}

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
}
}