Merge pull request #1610 from jerryyasir/master

This commit is contained in:
Hugo Bernier 2020-11-22 21:12:01 -05:00 committed by GitHub
commit 89867eab79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 36248 additions and 0 deletions

View File

@ -0,0 +1,68 @@
# Soccer Highlights Web Part
## Summary
- This react web part sample displays Soccer Highlights from a public Soccer API.
- It shows a maximum of 100 highlights at one time.
- The web part show live status of game scores and ability to watch them live in small or full screen view.
- You can view the highlights as FilmStrip Control (Thanks to Hugo for the tip and great blog) or Flat Mode.
- You can configure highlights per page and use Paging.
![Web Part](./assets/SoccerHighlightsV1.png)
### Web Part in Action
![Web Part in Action](./assets/SoccerHighlights.gif)
### Usage
1) Deploy the package to SharePoint Online App Catalog.
2) Add the Web Part to Page, Configure the web Part, provide Title and Page Size.
3) Add the Web Part to Page, Configure the web Part, provide Title and Page Size.
4) Click on Pager to move Pages or arrows or dots in filmstrip view.
## Used SharePoint Framework Version
![SPFx 1.11.0](https://img.shields.io/badge/version-1.11.0-green.svg)
## Applies to
- [SharePoint Framework](https://docs.microsoft.com/sharepoint/dev/spfx/sharepoint-framework-overview)
- [Office 365 tenant](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment)
## Prerequisites
None
## Solution
| Solution | Author(s) |
| --------------------- | ---------------------------------------- |
| Soccer Highlights Web Part | [Jerry Yasir](https://github.com/jyasir) |
## Version history
| Version | Date | Comments |
| ------- | ------------------ | ------------- |
| 1.0 | October 30, 2020 | First Version |
## 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 or download this repository
- Run in command line:
- `npm install` to install the npm dependencies
- `gulp serve` to display in Developer Workbench (recommend using your tenant workbench so you can test with real lists within your site)
- To package and deploy:
- Use `gulp bundle --ship` & `gulp package-solution --ship`
- Add the `.sppkg` to your SharePoint App Catalog
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/soccerhighlights" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-spfx-client-side-solution",
"id": "629b908d-ad61-4f35-9317-691a1f768908",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": ""
}
},
"paths": {
"zippedPackage": "solution/react-soccer-highlights.sppkg"
}
}

View File

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

View File

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

View File

@ -0,0 +1,58 @@
'use strict';
// check if gulp dist was called
if (process.argv.indexOf('dist') !== -1) {
// add ship options to command call
process.argv.push('--ship');
}
const path = require('path');
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const gulpSequence = require('gulp-sequence');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
// Create clean distrubution package
gulp.task('dist', gulpSequence('clean', 'bundle', 'package-solution'));
// Create clean development package
gulp.task('dev', gulpSequence('clean', 'bundle', 'package-solution'));
/**
* Custom Framework Specific gulp tasks
*/
const argv = build.rig.getYargs().argv;
const useCustomServe = argv['custom-serve'];
const fs = require("fs");
const workbenchApi = require("@microsoft/sp-webpart-workbench/lib/api");
if (useCustomServe) {
build.tslintCmd.enabled = false;
const ensureWorkbenchSubtask = build.subTask('ensure-workbench-task', function (gulp, buildOptions, done) {
this.log('Creating workbench.html file...');
try {
workbenchApi.default["/workbench"]();
} catch (e) { }
done();
});
build.rig.addPostBuildTask(build.task('ensure-workbench', ensureWorkbenchSubtask));
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
fs.writeFileSync("./temp/_webpack_config.json", JSON.stringify(generatedConfiguration, null, 2));
return generatedConfiguration;
}
});
}
build.initialize(require('gulp'));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
{
"name": "react-spfx",
"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",
"preversion": "node ./tools/pre-version.js",
"postversion": "gulp dist",
"serve": "cross-env NODE_OPTIONS=--max_old_space_size=4096 gulp bundle --custom-serve && cross-env NODE_OPTIONS=--max_old_space_size=4096 webpack-dev-server --mode development --config ./webpack.js --env.env=dev"
},
"dependencies": {
"@microsoft/sp-core-library": "1.11.0",
"@microsoft/sp-dialog": "^1.11.0",
"@microsoft/sp-lodash-subset": "1.11.0",
"@microsoft/sp-office-ui-fabric-core": "1.11.0",
"@microsoft/sp-property-pane": "1.11.0",
"@microsoft/sp-webpart-base": "1.11.0",
"@pnp/pnpjs": "^2.0.6",
"@pnp/spfx-controls-react": "2.1.0",
"@rehooks/component-size": "^1.0.3",
"axios": "^0.20.0",
"bootstrap": "^4.5.3",
"office-ui-fabric-react": "6.214.0",
"react": "16.8.5",
"react-dom": "16.8.5",
"react-markdown": "^5.0.2",
"react-paging": "^0.2.1",
"react-slick": "^0.27.13"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.3.5",
"@microsoft/sp-build-web": "1.11.0",
"@microsoft/sp-module-interfaces": "1.11.0",
"@microsoft/sp-tslint-rules": "1.11.0",
"@microsoft/sp-webpart-workbench": "1.11.0",
"@types/chai": "3.4.34",
"@types/es6-promise": "0.0.33",
"@types/mocha": "2.2.38",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"ajv": "~5.2.2",
"gulp": "~3.9.1",
"gulp-sequence": "1.0.0",
"css-loader": "3.4.2",
"css-modules-typescript-loader": "4.0.0",
"fork-ts-checker-webpack-plugin": "4.1.0",
"node-sass": "4.13.1",
"sass-loader": "8.0.2",
"style-loader": "1.1.3",
"ts-loader": "6.2.1",
"webpack": "4.42.0",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3",
"del": "5.1.0",
"cross-env": "7.0.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,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "0d3e8554-de4b-4b8c-bf6c-71591ffb926a",
"alias": "ReactSpFxWebPart",
"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": "Entertainment" },
"title": { "default": "Soccer Hightlights" },
"description": { "default": "Soccer Hightlights Web Parts show latest hightlights from around the world." },
"officeFabricIconFontName": "Soccer",
"properties": {
"title": "Soccer Highlights",
"pageSize": 5
}
}]
}

View File

@ -0,0 +1,84 @@
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 * as strings from 'ReactSpFxWebPartStrings';
import ReactSpFx from './components/ReactSpFx';
import { IReactSpFxProps } from './components/IReactSpFxProps';
import { DisplayMode } from '@microsoft/sp-core-library';
export interface IReactSpFxWebPartProps {
title: string;
pageSize: number;
displayMode: DisplayMode;
showFlatMode: boolean;
updateProperty: (value: string) => void;
}
export default class ReactSpFxWebPart extends BaseClientSideWebPart<IReactSpFxWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactSpFxProps> = React.createElement(
ReactSpFx,
{
title: this.properties.title,
displayMode: this.properties.displayMode,
pageSize: this.properties.pageSize == undefined ? 10 : this.properties.pageSize,
showFlatMode: this.properties.showFlatMode,
updateProperty: (value: string) => {this.properties.title = value;},
onConfigure: () => {
this.context.propertyPane.open();
},
}
);
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: [
PropertyPaneSlider('pageSize',{
label:"Highlights Per Page",
min:1,
max:20,
value:5,
showValue:true,
step:1
}),
PropertyPaneToggle('showFlatMode', {
label: "Show Videos in Flat Mode",
checked: false
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,62 @@
import { DisplayMode } from '@microsoft/sp-core-library';
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
export interface IReactSpFxProps {
title: string;
displayMode: DisplayMode;
pageSize: number;
showFlatMode: boolean;
updateProperty: (value: string) => void;
onConfigure: () => void;
}
export interface IVideosList {
videos: IVideo[];
}
export interface IVideo {
title: string;
embed: string;
}
export interface ISportsHighlightsState {
sportHighlightState: any;
}
export interface ISportsHighlightPagingState {
pagedSportHighlights?: ISportsHighlightProps[];
slicedSportHighlights?: ISportsHighlightProps[];
}
export interface ISportsHightLightsProps {
pageSize:number;
showFlatMode: boolean;
}
export interface ISide {
name: string;
}
export interface ICompetition{
name: string;
id: number;
}
export interface ISportsHighlights {
highLights?: ISportsHighlightProps[];
}
export interface ISportsHighlightProps {
title: string;
embed: string;
id: string;
date: Date;
side1: ISide;
side2: ISide;
competition: ICompetition;
thumbnail: string;
videos: IVideo[];
url: string;
}

View File

@ -0,0 +1,25 @@
import React from "react";
const Pagination = ({ highLightsPerPage, totalHighlights, paginate }) => {
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(totalHighlights / highLightsPerPage); i++) {
pageNumbers.push(i);
}
return (
<nav>
<ul className="pagination" style={{ marginLeft: "10px" }}>
{pageNumbers.map((number) => (
<li key={number} className="page-item">
<a onClick={() => paginate(number)} href="#" className="page-link">
{number}
</a>
</li>
))}
</ul>
</nav>
);
};
export default Pagination;

View File

@ -0,0 +1,86 @@
@import '~office-ui-fabric-react/dist/sass/References.scss';
.reactSpFx {
.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);
}
.paginationDiv{
border: 1px solid lightblue;
height: 100px;
padding: 10px 0;
width: 100%;
display: flex;
justify-content: center;
}
.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 {
font-size: large;
font-weight: bold;
font-style: normal;
@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;
}
}
}

View File

@ -0,0 +1,17 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'button': string;
'column': string;
'container': string;
'description': string;
'label': string;
'ms-Grid': string;
'paginationDiv': string;
'reactSpFx': string;
'row': string;
'subTitle': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,28 @@
import * as React from "react";
import styles from "./ReactSpFx.module.scss";
import { IReactSpFxProps, ISportsHighlightsState } from "./IReactSpFxProps";
import SportsHighlightsList from "./SportsHighlightsList";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import "bootstrap/dist/css/bootstrap.css";
export default class ReactSpFx extends React.Component<IReactSpFxProps> {
public constructor(props: IReactSpFxProps, state: ISportsHighlightsState) {
super(props);
}
public render(): React.ReactElement<IReactSpFxProps> {
return (
<div className={styles.container}>
<WebPartTitle
displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty}
/>
<SportsHighlightsList pageSize={this.props.pageSize} showFlatMode={this.props.showFlatMode}
/>
</div>
);
}
}

View File

@ -0,0 +1,94 @@
import React from "react";
import { ISportsHighlightProps, ISportsHighlights } from "./IReactSpFxProps";
import { FilmstripLayout } from './filmstripLayout/index';
import styles from './filmstripLayout/FilmstripLayout.module.scss';
import {
DocumentCard,
DocumentCardActivity,
DocumentCardPreview,
DocumentCardDetails,
DocumentCardTitle,
IDocumentCardPreviewProps,
DocumentCardType,
} from "office-ui-fabric-react/lib/DocumentCard";
import { ImageFit } from "office-ui-fabric-react/lib/Image";
export interface IDialogState
{
isDialogOpen: boolean;
}
export default class SportVideoListFilmStripView extends React.Component<
ISportsHighlights,
IDialogState
> {
constructor(props: ISportsHighlights, state: IDialogState) {
super(props);
this.state = {
isDialogOpen: false,
};
}
public render(): React.ReactElement<ISportsHighlights> {
const videos = this.props.highLights;
console.log("Video Highlights", videos);
return (
<div>
<div className={styles.filmStrip}>
<FilmstripLayout
ariaLabel={
"Soccer highlights web part, showing soccer highlights. Use right and left arrow keys to navigate between cards in the film strip."
}
>
{videos.map((video: ISportsHighlightProps, i) => {
let videoDate = new Date(video.date.toString());
const previewProps: IDocumentCardPreviewProps = {
previewImages: [
{
previewImageSrc: video.thumbnail,
imageFit: ImageFit.cover,
height: 130,
},
],
};
return (
<div
key={i}
data-is-focusable={true}
role="listitem"
aria-label={video.title}
>
<DocumentCard
type={DocumentCardType.normal}
onClickHref={video.url}
>
<DocumentCardPreview {...previewProps} />
<DocumentCardDetails>
<DocumentCardTitle
title={video.side1.name+ ' vs ' + video.side2.name}
shouldTruncate={true}
/>
<DocumentCardActivity
activity={
videoDate.toLocaleString()
}
people={[
{
name: video.competition.name,
profileImageSrc: video.thumbnail,
},
]}
/>
</DocumentCardDetails>
</DocumentCard>
</div>
);
})}
</FilmstripLayout>
</div>
</div>
);
}
}

View File

@ -0,0 +1,45 @@
import { videoProperties } from "office-ui-fabric-react";
import React from "react";
import ReactDOM from "react-dom";
import { ISportsHighlightProps } from "./IReactSpFxProps";
import SportsVideoList from "./SportsVideoList";
export default class SportsHighlight extends React.Component<
ISportsHighlightProps
> {
public render() {
const options = {
year: "2-digit",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
};
const {
title,
id,
date,
side1,
side2,
competition,
thumbnail,
videos,
} = this.props;
return (
<div style={{ margin: "2px" }}>
<div style={{ display: "inline-block", marginLeft: "12px" }}>
<div style={{ fontSize: "1.25rem", fontWeight: "bold" }}>{title}</div>
<div style={{ fontSize: "16px", fontWeight: "bold" }}>
{competition.name}
</div>
<div style={{ fontSize: "14px", fontWeight: "normal" }}>
Date & Time: <b>{new Date(date.toString()).toLocaleString("en-US", options)}</b>
</div>
<div>{id}</div>
<SportsVideoList videos={videos} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,92 @@
import React from "react";
import {
ISportsHightLightsProps,
ISportsHighlightPagingState,
ISportsHighlightProps,
} from "./IReactSpFxProps";
import SportVideoListFilmStripView from "./SportVideoListFilmStripView";
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import axios from "axios";
import SportsVideoList from "./SportsVideoList";
export default class SportsHighlightsList extends React.Component<
ISportsHightLightsProps,
ISportsHighlightPagingState
> {
constructor(
props: ISportsHightLightsProps,
state: ISportsHighlightPagingState
) {
super(props);
this.state = {
pagedSportHighlights: [],
slicedSportHighlights: [],
};
}
public GetData = async () => {
const resp = await axios.get(`https://www.scorebat.com/video-api/v1/`);
const data : ISportsHighlightProps[] = await resp.data;
let slicedItems : ISportsHighlightProps[] = data.slice(0, this.props.pageSize);
this.setState({ pagedSportHighlights: data, slicedSportHighlights: slicedItems });
}
public async componentDidMount() {
console.log("Mounted");
this.GetData();
}
public render(): React.ReactElement<ISportsHightLightsProps> {
if(!this.props.showFlatMode){
return <SportVideoListFilmStripView highLights={this.state.slicedSportHighlights} />;
}
let pageCountDivisor: number = this.props.pageSize;
let pageCount: number;
let pageButtons = [];
let highlightItems = this.state.pagedSportHighlights;
let _pagedButtonClick = (pageNumber: number, listData: any) => {
let startIndex: number = (pageNumber - 1) * pageCountDivisor;
let endIndex = startIndex + pageCountDivisor;
let listItemsCollection = [...listData];
let slicedItems: ISportsHighlightProps[] = listItemsCollection.slice(
startIndex,
endIndex
);
this.setState({
slicedSportHighlights: slicedItems
});
};
var pagedItems: JSX.Element = (<SportsVideoList videos={this.state.slicedSportHighlights} /> );
if (highlightItems.length > 0) {
pageCount = Math.ceil(highlightItems.length / pageCountDivisor);
}
for (let i = 0; i < pageCount; i++) {
pageButtons.push(
<PrimaryButton key={i} style={{width:"50px"}}
onClick={() => {
_pagedButtonClick(i + 1, highlightItems);
}}
>
{" "}
{i + 1}{" "}
</PrimaryButton>
);
}
return (
<div>
<div className={`ms-Grid-row`} style={{paddingLeft:"8px"}}>
<div className="ms-Grid-col ms-u-lg12">{pageButtons}</div>
</div>
<div className="ms-Grid-row">
<div className="ms-Grid-col ms-u-lg12">{pagedItems}</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,26 @@
import React from "react";
import ReactDOM from "react-dom";
import { IVideo } from "./IReactSpFxProps";
import ReactMarkdownWithHtml from "react-markdown/with-html";
import { body } from "@pnp/pnpjs";
export default class SportsVideo extends React.Component<IVideo> {
public render() {
const { title, embed } = this.props;
return (
<div>
<div
style={{ fontSize: "18px", fontWeight: "bold", textAlign: "center", color:"blue" }}
title={"click to play " + title}
>
{title}
</div>
<div id={"video" + title}
dangerouslySetInnerHTML={{ __html: embed }}
style={{ padding: "8px" }}
title={"click to play " + title}
/>
</div>
);
}
}

View File

@ -0,0 +1,22 @@
import React from "react";
import ReactDOM from "react-dom";
import { IVideosList } from "./IReactSpFxProps";
import SportsVideo from "./SportsVideo";
import ReactMarkdownWithHtml from "react-markdown/with-html";
export default class SportsVideoList extends React.Component<IVideosList> {
public render() {
const videos = this.props.videos;
return (
<div style={{ margin: "1px", alignContent:"center" }}>
{this.props.videos.map((video, i) => (
<SportsVideo
key={i}
embed={video.embed}
title={video.title}
/>
))}
</div>
);
}
}

View File

@ -0,0 +1,117 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
:export {
centerPadding: 10px;
}
.filmstripLayout {
position: relative;
&.filmStrip {
margin-bottom: 27px;
margin-left: -10px;
margin-right: -10px;
:global(.slick-slide) {
box-sizing: border-box;
padding: 0 10px;
}
}
.sliderButtons {
opacity: 0;
}
&:hover .sliderButtons {
opacity: 1;
&:hover {
color: $ms-color-white;
}
}
}
.indexButtonContainer {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
}
.indexButton {
font-size: 28px;
font-weight: 400;
height: 40px;
padding: 0;
border: 0;
background: 0 0;
cursor: pointer;
color: $ms-color-white;
width: 40px;
min-width: 20px;
margin-left: 0;
line-height: 40px;
box-sizing: content-box;
background-color: $ms-color-black;
opacity: 0.6;
position: absolute;
top: 50%;
transform: translateY(-50%);
transition: all 0.3s;
&:hover {
color: $ms-color-white;
background-color: $ms-color-black;
opacity: 0.6;
}
&:active {
outline: -webkit-focus-ring-color auto 1px;
}
}
.carouselDotsContainer {
.carouselDot {
display: inline-block;
background-color: $ms-color-black;
height: 4px;
width: 4px;
opacity: 0.5;
border: 2px solid $ms-color-black;
border-radius: 4px;
cursor: pointer;
opacity: 0.25;
outline: 0;
&:hover {
opacity: 1;
}
}
}
:global(.slick-active) {
.carouselDotsContainer {
.carouselDot {
background-color: "[theme:themeDark, default: #005a9e]";
opacity: 0.75;
border-color: "[theme:themeDark, default: #005a9e]";
&:hover {
opacity: 1;
}
}
}
}
.indexButton:global(.ms-Button-flexContainer):hover:global(.ms-Icon),
.indexButton:global(.ms-Icon:hover),
.indexButton:hover:global(.ms-Icon) {
color: $ms-color-white;
}
.leftPositioned {
left: 0;
}
.rightPositioned {
right: 0;
}

View File

@ -0,0 +1,16 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'carouselDot': string;
'carouselDotsContainer': string;
'centerPadding': string;
'filmStrip': string;
'filmstripLayout': string;
'indexButton': string;
'indexButtonContainer': string;
'leftPositioned': string;
'rightPositioned': string;
'sliderButtons': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -0,0 +1,117 @@
import { css } from "@uifabric/utilities/lib/css";
import { IconButton } from "office-ui-fabric-react/lib/Button";
import * as React from "react";
import Slider from "react-slick";
import { SPComponentLoader } from "@microsoft/sp-loader";
import styles from "./FilmstripLayout.module.scss";
import { useRef } from "react";
import useComponentSize, { ComponentSize } from "@rehooks/component-size";
/**
* Filmstrip layout
* Presents the child compoments as a slick slide
*/
export const FilmstripLayout = (props: {
children: any;
ariaLabel?: string;
}) => {
let ref: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(
null
);
let size: ComponentSize = useComponentSize(ref);
let { width } = size;
// // the slick slider used in normal views
let _slider: React.MutableRefObject<Slider> = useRef<Slider>(null);
SPComponentLoader.loadCss(
"https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick.min.css"
);
SPComponentLoader.loadCss(
"https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.8.1/slick-theme.min.css"
);
// slick seems to have an issue with having "infinite" mode set to true and having less items than the number of slides per page
// set infinite to true only if there are more than 3 children
let numSlides: number = 3;
if (width) {
if (width > 927) {
numSlides = 4;
} else if (width <= 695) {
numSlides = 2;
}
}
var isInfinite: boolean = React.Children.count(props.children) > numSlides;
var settings: any = {
accessibility: true,
arrows: false,
autoplaySpeed: 5000,
dots: true,
customPaging: (i: number) => {
return (
<a>
<div
role="button"
className={styles.carouselDotsContainer}
aria-label={`Carousel Dot ${i}`}
data-is-focusable={true}
tabIndex={0}
>
<span className={styles.carouselDot} tabIndex={-1}></span>
</div>
</a>
);
},
infinite: isInfinite,
slidesToShow: numSlides,
slidesToScroll: numSlides,
speed: 500,
centerPadding: styles.centerPadding,
pauseOnHover: true,
variableWidth: false,
useCSS: true,
rows: 1,
respondTo: "slider",
};
return (
<div>
<div
className={css(styles.filmstripLayout, styles.filmStrip)}
aria-label={props.ariaLabel}
ref={ref}
>
<Slider ref={_slider} {...settings}>
{props.children}
</Slider>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ left: "10px" }}
onClick={() => _slider.current.slickPrev()}
>
<IconButton
className={css(styles.indexButton, styles.leftPositioned)}
iconProps={{
iconName: "ChevronLeft",
styles: { root: { fontSize: "28px", fontWeight: "400" } },
}}
/>
</div>
<div
className={css(styles.indexButtonContainer, styles.sliderButtons)}
style={{ right: "10px" }}
onClick={() => _slider.current.slickNext()}
>
<IconButton
className={css(styles.indexButton, styles.rightPositioned)}
iconProps={{
iconName: "ChevronRight",
styles: { root: { fontSize: "28px", fontWeight: "400" } },
}}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./FilmstripLayout";

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Display the highlights and goals of the latest soccer matches ",
"BasicGroupName": "General",
"DescriptionFieldLabel": "Title"
}
});

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

View File

@ -0,0 +1,64 @@
/**
* This script updates the package-solution version analogue to the
* the package.json file.
*/
if (process.env.npm_package_version === undefined) {
throw 'Package version cannot be evaluated';
}
// define path to package-solution file
const solution = './config/package-solution.json',
teams = './teams/manifest.json';
// require filesystem instance
const fs = require('fs');
// get next automated package version from process variable
const nextPkgVersion = process.env.npm_package_version;
// make sure next build version match
const nextVersion = nextPkgVersion.indexOf('-') === -1 ?
nextPkgVersion : nextPkgVersion.split('-')[0];
// Update version in SPFx package-solution if exists
if (fs.existsSync(solution)) {
// read package-solution file
const solutionFileContent = fs.readFileSync(solution, 'UTF-8');
// parse file as json
const solutionContents = JSON.parse(solutionFileContent);
// set property of version to next version
solutionContents.solution.version = nextVersion + '.0';
// save file
fs.writeFileSync(
solution,
// convert file back to proper json
JSON.stringify(solutionContents, null, 2),
'UTF-8');
}
// Update version in teams manifest if exists
if (fs.existsSync(teams)) {
// read package-solution file
const teamsManifestContent = fs.readFileSync(teams, 'UTF-8');
// parse file as json
const teamsContent = JSON.parse(teamsManifestContent);
// set property of version to next version
teamsContent.version = nextVersion;
// save file
fs.writeFileSync(
teams,
// convert file back to proper json
JSON.stringify(teamsContent, null, 2),
'UTF-8');
}

View File

@ -0,0 +1,40 @@
{
"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"
],
"esModuleInterop": true
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"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
}
}

View File

@ -0,0 +1,259 @@
const path = require("path");
const fs = require("fs");
const webpack = require("webpack");
const resolve = require("path").resolve;
const CertStore = require("@microsoft/gulp-core-build-serve/lib/CertificateStore");
const CertificateStore = CertStore.CertificateStore || CertStore.default;
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const del = require("del");
const port = 4321;
const host = "https://localhost:" + port;
///
// Transforms define("<guid>", ...) to web part specific define("<web part id_version", ...)
// the same approach is used inside copyAssets SPFx build step
///
class DynamicLibraryPlugin {
constructor(options) {
this.opitons = options;
}
apply(compiler) {
compiler.hooks.emit.tap("DynamicLibraryPlugin", compilation => {
for (const assetId in this.opitons.modulesMap) {
const moduleMap = this.opitons.modulesMap[assetId];
if (compilation.assets[assetId]) {
const rawValue = compilation.assets[assetId].children[0]._value;
compilation.assets[assetId].children[0]._value = rawValue.replace(this.opitons.libraryName, moduleMap.id + "_" + moduleMap.version);
}
}
});
}
}
///
// Removes *.module.scss.ts on the first execution in order prevent conflicts with *.module.scss.d.ts
// generated by css-modules-typescript-loader
///
class ClearCssModuleDefinitionsPlugin {
constructor(options) {
this.options = options || {};
}
apply(compiler) {
compiler.hooks.done.tap("FixStylesPlugin", stats => {
if (!this.options.deleted) {
setTimeout(() => {
del.sync(["src/**/*.module.scss.ts"]);
}, 3000);
this.options.deleted = true;
}
});
}
}
let baseConfig = {
target: "web",
mode: "development",
devtool: "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js"],
modules: ["node_modules"]
},
context: path.resolve(__dirname),
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true,
compilerOptions: {
declarationMap: false
}
},
exclude: /node_modules/
},
{
use: [{
loader: "@microsoft/loader-cased-file",
options: {
name: "[name:lower]_[hash].[ext]"
}
}],
test: /\.(jpe?g|png|woff|eot|ttf|svg|gif|dds)$/i
},
{
use: [{
loader: "html-loader"
}],
test: /\.html$/
},
{
test: /\.css$/,
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
{
loader: "css-loader"
}
]
},
{
test: function (fileName) {
return fileName.endsWith(".module.scss"); // scss modules support
},
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
"css-modules-typescript-loader",
{
loader: "css-loader",
options: {
modules: {
localIdentName: "[local]_[hash:base64:8]"
}
}
}, // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
]
},
{
test: function (fileName) {
return !fileName.endsWith(".module.scss") && fileName.endsWith(".scss"); // just regular .scss
},
use: [
{
loader: "@microsoft/loader-load-themed-styles",
options: {
async: true
}
},
"css-loader", // translates CSS into CommonJS
"sass-loader" // compiles Sass to CSS, using Node Sass by default
]
}
]
},
plugins: [
new ForkTsCheckerWebpackPlugin({
tslint: true
}),
new ClearCssModuleDefinitionsPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.DEBUG": JSON.stringify(true),
"DEBUG": JSON.stringify(true)
})],
devServer: {
hot: false,
contentBase: resolve(__dirname),
publicPath: host + "/dist/",
host: "localhost",
port: port,
disableHostCheck: true,
historyApiFallback: true,
open: true,
writeToDisk: false,
openPage: host + "/temp/workbench.html",
stats: {
preset: "errors-only",
colors: true,
chunks: false,
modules: false,
assets: false
},
proxy: { // url re-write for resources to be served directly from src folder
"/lib/**/loc/*.js": {
target: host,
pathRewrite: { "^/lib": "/src" },
secure: false
}
},
headers: {
"Access-Control-Allow-Origin": "*",
},
https: {
cert: CertificateStore.instance.certificateData,
key: CertificateStore.instance.keyData
}
},
}
const createConfig = function () {
// remove old css module TypeScript definitions
del.sync(["dist/*.js", "dist/*.map"]);
// we need only "externals", "output" and "entry" from the original webpack config
let originalWebpackConfig = require("./temp/_webpack_config.json");
baseConfig.externals = originalWebpackConfig.externals;
baseConfig.output = originalWebpackConfig.output;
baseConfig.entry = getEntryPoints(originalWebpackConfig.entry);
baseConfig.output.publicPath = host + "/dist/";
const manifest = require("./temp/manifests.json");
const modulesMap = {};
const originalEntries = Object.keys(originalWebpackConfig.entry);
for (const jsModule of manifest) {
if (jsModule.loaderConfig
&& jsModule.loaderConfig.entryModuleId
&& originalEntries.indexOf(jsModule.loaderConfig.entryModuleId) !== -1) {
modulesMap[jsModule.loaderConfig.entryModuleId + ".js"] = {
id: jsModule.id,
version: jsModule.version
}
}
}
baseConfig.plugins.push(new DynamicLibraryPlugin({
modulesMap: modulesMap,
libraryName: originalWebpackConfig.output.library
}));
return baseConfig;
}
function getEntryPoints(entry) {
// fix: ".js" entry needs to be ".ts"
// also replaces the path form /lib/* to /src/*
let newEntry = {};
let libSearchRegexp;
if (path.sep === "/") {
libSearchRegexp = /\/lib\//gi;
} else {
libSearchRegexp = /\\lib\\/gi;
}
const srcPathToReplace = path.sep + "src" + path.sep;
for (const key in entry) {
let entryPath = entry[key];
if (entryPath.indexOf("bundle-entries") === -1) {
entryPath = entryPath.replace(libSearchRegexp, srcPathToReplace).slice(0, -3) + ".ts";
} else {
// replace paths and extensions in bundle file
let bundleContent = fs.readFileSync(entryPath).toString();
bundleContent = bundleContent.replace(libSearchRegexp, srcPathToReplace).replace(/\.js/gi, ".ts");
fs.writeFileSync(entryPath, bundleContent);
}
newEntry[key] = entryPath;
}
return newEntry;
}
module.exports = createConfig();

File diff suppressed because it is too large Load Diff