Merge pull request #1221 from hugoabernier/joaojmendes-react-global-news-2

Merging #1218 and #1220
This commit is contained in:
Hugo Bernier 2020-04-15 13:37:05 -04:00 committed by GitHub
commit 07c7e9e6e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 32363 additions and 1 deletions

View File

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

32
samples/react-global-news/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,13 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.10.0",
"libraryName": "react-global-news",
"libraryId": "ee849c9a-efd9-4257-99d0-ad0b1c0fb095",
"environment": "spo",
"packageManager": "npm",
"framework": "react",
"isCreatingSolution": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,84 @@
---
page_type: sample
products:
- office-sp
languages:
- javascript
- typescript
extensions:
contentType: samples
technologies:
- SharePoint Framework
- Microsoft Graph
platforms:
- React
createdDate: 4/14/2020 12:00:00 AM
---
# React Global News
## Summary
This web part show a world news from various sources, it uses the API available on https://newsapi.org this collect information from news and blog sites arround the world.
Please go to https://newsapi.org to get more information.
![picture of the web part in action](assets/globalNews.gif)
![picture of the web part in action](assets/GlobalNews.png)
## Used SharePoint Framework Version
![1.10.0](https://img.shields.io/badge/version-1.10.0-green.svg)
## Applies to
* [SharePoint Framework](https:/dev.office.com/sharepoint)
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
## Prerequisites
> none
## Solution
Solution|Author(s)
--------|---------
react-global-news | João Mendes
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 14, 2020|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Minimal Path to Awesome
* Clone this repository
* in the command line run:
* `npm install`
* `gulp serve`
> Include any additional steps as needed.
## Features
Description of the web part with possible additional details than in short summary.
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Office-ui-fabric-react components
* Office-ui-fabric themes
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-global-news" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,14 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-global-news-client-side-solution",
"id": "ee849c9a-efd9-4257-99d0-ad0b1c0fb095",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-global-news.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 -->"
}

54
samples/react-global-news/gulpfile.js vendored Normal file
View File

@ -0,0 +1,54 @@
'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'));
/**
* Webpack Bundle Anlayzer
* Reference and gulp task
*/
const bundleAnalyzer = require('webpack-bundle-analyzer');
build.configureWebpack.mergeConfig({
additionalConfiguration: (generatedConfiguration) => {
const lastDirName = path.basename(__dirname);
const dropPath = path.join(__dirname, 'temp', 'stats');
generatedConfiguration.plugins.push(new bundleAnalyzer.BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: path.join(dropPath, `${lastDirName}.stats.html`),
generateStatsFile: true,
statsFilename: path.join(dropPath, `${lastDirName}.stats.json`),
logLevel: 'error'
}));
return generatedConfiguration;
}
});
/**
* Custom Framework Specific gulp tasks
*/
build.initialize(gulp);

18144
samples/react-global-news/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
{
"name": "react-global-news",
"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.9.10",
"@material-ui/lab": "^4.0.0-alpha.49",
"@microsoft/sp-core-library": "1.10.0",
"@microsoft/sp-lodash-subset": "1.10.0",
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
"@microsoft/sp-property-pane": "1.10.0",
"@microsoft/sp-webpart-base": "1.10.0",
"@pnp/spfx-controls-react": "^1.17.0",
"@pnp/spfx-property-controls": "^1.17.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.8.8",
"@types/react-dom": "16.8.3",
"@types/webpack-env": "1.13.1",
"gulp-sequence": "^1.0.0",
"jquery": "^3.5.0",
"lodash": "^4.17.15",
"material-ui": "^0.20.2",
"moment": "^2.24.0",
"office-ui-fabric-react": "6.189.2",
"react": "16.8.5",
"react-dom": "16.8.5",
"spfx-uifabric-themes": "^0.8.0",
"webpack-bundle-analyzer": "^3.7.0"
},
"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,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#0b0b0b",
"neutralLighter": "#151515",
"neutralLight": "#252525",
"neutralQuaternaryAlt": "#2f2f2f",
"neutralQuaternary": "#373737",
"neutralTertiaryAlt": "#595959",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#000000"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#2d2c2c",
"neutralLighter": "#2c2b2b",
"neutralLight": "#2a2929",
"neutralQuaternaryAlt": "#272626",
"neutralQuaternary": "#252525",
"neutralTertiaryAlt": "#242323",
"neutralTertiary": "#c8c8c8",
"neutralSecondary": "#d0d0d0",
"neutralPrimaryAlt": "#dadada",
"neutralPrimary": "#ffffff",
"neutralDark": "#f4f4f4",
"black": "#f8f8f8",
"white": "#2d2c2c"
}

View File

@ -0,0 +1,24 @@
{
"themePrimary": "#6264a7",
"themeLighterAlt": "#f7f7fb",
"themeLighter": "#e1e1f1",
"themeLight": "#c8c9e4",
"themeTertiary": "#989ac9",
"themeSecondary": "#7173b0",
"themeDarkAlt": "#585a95",
"themeDark": "#4a4c7e",
"themeDarker": "#37385d",
"neutralLighterAlt": "#ecebe9",
"neutralLighter": "#e8e7e6",
"neutralLight": "#dedddc",
"neutralQuaternaryAlt": "#cfcecd",
"neutralQuaternary": "#c6c5c4",
"neutralTertiaryAlt": "#bebdbc",
"neutralTertiary": "#b5b4b2",
"neutralSecondary": "#9d9c9a",
"neutralPrimaryAlt": "#868482",
"neutralPrimary": "#252423",
"neutralDark": "#565453",
"black": "#3e3d3b",
"white": "#f3f2f1"
}

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,13 @@
export interface IArticle {
source: {
id: string;
name: string;
};
author: string;
title: string;
description: string;
url: string;
urlToImage: string;
publishedAt: string;
content: string;
}

View File

@ -0,0 +1,8 @@
import { IArticle } from "./IArticle";
export interface INewsResults {
status: string;
totalResults: number;
articles: IArticle[];
}

View File

@ -0,0 +1,5 @@
export interface INewsResultsError {
status: string;
code: string;
message: string;
}

View File

@ -0,0 +1,11 @@
export interface ISource {
id: string;
name: string;
description: string;
url: string;
category: string;
language: string;
country: string;
}

View File

@ -0,0 +1,6 @@
import { ISource } from "./ISource";
export interface ISourcesResults {
status: string;
sources: ISource[];
}

View File

@ -0,0 +1,127 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import {
HttpClient,
HttpClientResponse,
IHttpClientOptions
} from "@microsoft/sp-http";
import { INewsResults } from "./INewsResults";
import { ISourcesResults } from "./ISourcesResults";
export default class dataservices {
private static _httpClient: HttpClient;
/*
initialize the static class
*/
public static async init(context: WebPartContext) {
//obtain the httpClient from the webpart context
this._httpClient = context.httpClient;
}
// Get Sources
public static async getSources(apiKey:string):Promise<ISourcesResults>{
let requestHeaders = new Headers();
let sourceUrl = `https://newsapi.org/v2/sources`;
let _corsUrl = `https://cors-anywhere.herokuapp.com/${sourceUrl}`;
if (!sourceUrl || !apiKey) return;
try {
let _url = new URL(sourceUrl);
let _count = 0;
_url.searchParams.forEach((v,k,p) =>{
_count ++;
});
if (_count !== 0 ) { // test if has parameters
_corsUrl = _corsUrl + `&apiKey=${apiKey}`; // has parameters addpikey to last one
} else {
_corsUrl = _corsUrl.replace('?','') + `?apiKey=${apiKey}`; // add parameter apikey
}
} catch (error) {
return; // not valid Url
}
requestHeaders.append("Accept", "application/json");
requestHeaders.append("content-type", "application/json");
try {
//set up get options
const requestGetOptions: IHttpClientOptions = {
method: "GET",
headers: requestHeaders,
mode: "cors"
};
let query: HttpClientResponse = await this._httpClient.fetch(
_corsUrl,
HttpClient.configurations.v1,
requestGetOptions
);
return await query.json();
} catch (error) {
console.log(error);
throw new Error('Is not possible to read news at this moment, please try later.');
}
}
// Get News
public static async getNews(
newsUrl: string,
apiKey: string,
page?:number,
): Promise<INewsResults> {
let requestHeaders = new Headers();
const defaultPageSize = 12;
let _page:number = page ? page : 1;
let _corsUrl = `https://cors-anywhere.herokuapp.com/${newsUrl}&page=${_page}`;
if (!newsUrl || !apiKey) return;
try {
let _url = new URL(newsUrl);
let _count = 0;
_url.searchParams.forEach((v,k,p) =>{
_count ++;
});
if (_count !== 0 ) { // test if has parameters
_corsUrl = _corsUrl + `&apiKey=${apiKey}`; // has parameters addpikey to last one
} else {
_corsUrl = _corsUrl.replace('?','') + `?apiKey=${apiKey}`; // add parameter apikey
}
// teste if has pagesize parameter must be equal to default parameter value of web part
if ( !_url.searchParams.has('pagesize')){
_corsUrl = _corsUrl + `&pageSize=${defaultPageSize}`; //
}
} catch (error) {
return; // not valid Url
}
requestHeaders.append("Accept", "application/json");
requestHeaders.append("content-type", "application/json");
try {
//set up get options
const requestGetOptions: IHttpClientOptions = {
method: "GET",
headers: requestHeaders,
mode: "cors"
};
let query: HttpClientResponse = await this._httpClient.fetch(
_corsUrl,
HttpClient.configurations.v1,
requestGetOptions
);
return await query.json();
} catch (error) {
console.log(error);
throw new Error('Is not possible to read news at this moment, please try later.');
}
}
}

View File

@ -0,0 +1,15 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { DisplayMode } from "@microsoft/sp-core-library";
import { IReadonlyTheme } from '@microsoft/sp-component-base';
export interface INewsProps {
title: string;
newsUrl: string;
apiKey: string;
context: WebPartContext;
updateProperty: (value: string) => void;
displayMode: DisplayMode;
viewOption:string;
pageSize: number;
themeVariant: IReadonlyTheme | undefined;
}

View File

@ -0,0 +1,10 @@
import { IArticle } from "../../services/IArticle";
export interface INewsState{
hasError:boolean;
articles: IArticle[];
errorMesage:string;
isLoading: boolean;
currentPage:number;
totalPages:number;
}

View File

@ -0,0 +1,370 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@import "./node_modules/spfx-uifabric-themes/office.theme";
@import "~office-ui-fabric-react/dist/sass/semanticSlots";
@media screen and (min-width: 320px) and (max-width: 1024px) {
.card {
flex-direction: column;
}
.image {
width: 100%;
height: auto;
}
.newsTitle {
margin-top: 20px;
}
}
/* mixin for multiline */
@mixin multiLineEllipsis($lineHeight: 1.5em, $lineCount: 1, $bgColor: white) {
overflow: hidden;
position: relative;
line-height: $lineHeight;
max-height: $lineHeight * $lineCount;
text-align: start;
/* @include ms-fontWeight-semibold; */
color: $ms-color-neutralSecondaryAlt;
@include ms-fontSize-m;
&:before {
content: "...";
position: absolute;
right: 0;
bottom: 0;
}
&:after {
content: "";
position: absolute;
right: 0;
width: 1em;
height: 1em;
margin-top: 0.2em;
/* background: $bgColor;*/
}
}
.News {
/*:global { */
padding: 10px;
.cardsTiles {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, auto));
column-gap: 1rem;
row-gap: 1.5rem;
grid-auto-rows: auto;
}
.cardsItem {
width: 100%;
margin-bottom: 20px;
display: grid;
grid-template-columns: auto;
grid-template-rows: auto;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin-right: 15px;
margin-left: 15px;
margin-top: 10px;
margin-bottom: 7px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.separator {
border-style: solid;
border-width: 1px;
border-color: #edebe9;
width: 100%;
margin-bottom: 10px;
}
}
.image {
object-fit: cover;
}
.newsTitle {
padding-top: 10px;
overflow: hidden;
margin-left: 16px;
margin-right: 16px;
text-overflow: ellipsis;
max-width: 100%;
@include ms-fontWeight-semibold;
text-align: start;
text-transform: uppercase;
}
.secondaryText {
position: relative;
display: block;
overflow: hidden;
padding-top: 10px;
margin-left: 16px;
margin-right: 16px;
height: 55px;
}
.container {
display: flex;
flex-direction: column;
margin-top: 15px;
max-width: 100%;
}
[data-theme="default"] {
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
}
[data-theme="dark"] {
.nonewsIcon {
color: $ms-color-white;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralSecondaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.secondaryText {
position: relative;
display: block;
overflow: hidden;
padding-top: 10px;
color: $ms-color-neutralTertiaryAlt;
margin-left: 16px;
margin-right: 16px;
height: 55px;
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
}
[data-theme="contrast"] {
.nonewsIcon {
color: $ms-color-white;
}
.card {
/* box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); */
display: Flex;
flex-direction: row;
height: 180px;
min-width: 280px;
align-items: flex-start;
margin: 5px;
border-style: solid;
border-width: 1px;
border-color: $ms-color-neutralTertiaryAlt;
}
.card:hover {
border-color: $ms-color-neutralTertiary;
cursor: pointer;
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
}
.author {
margin-top: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.date {
@include ms-font-s-plus;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 15px;
}
.nonewsIcon {
font-size: 96px;
/* color: $ms-color-neutralPrimary;*/
width: 96px;
height: 96px;
}
.nonewsMessage {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-top: 0px;
/* color: $ms-color-neutralPrimary;*/
max-width: 500px;
@include ms-fontWeight-semibold;
}
.title {
margin-left: 15px;
}
/*}*/
:global {
// default theme
$default-background: #f3f2f1;
$default-color: #252423;
$default-button-background: #6264a7;
$default-Button-color: #f3f2f1;
// dark theme
$dark-background: #2d2c2c;
$dark-color: #ffffff;
$dark-button-background: #6264a7;
$dark-button-color: #2d2c2c;
// contrast theme
$contrast-background: #000000;
$contrast-color: #ffffff;
$contrast-button-background: #b5c01c;
$contrast-Button-color: #000000;
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $ms-color-neutralPrimary !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $ms-color-neutralPrimary !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $ms-color-white !important;
background-color: $ms-themePrimary !important;
}
.MuiPaginationItem-page:hover {
background-color: $ms-themePrimary !important;
}
[data-theme="dark"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $dark-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $dark-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $dark-color !important;
background-color: $dark-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $dark-button-background !important;
}
.ms-Dropdown-optionText {
color: $ms-color-white !important;
}
[class^="propertyPaneFooter_"] {
background-color: $dark-background;
}
}
[data-theme="default"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $default-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $default-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $ms-color-white !important;
background-color: $default-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $default-button-background !important;
}
[class^="propertyPaneFooter_"] {
background-color: $default-background;
}
}
[data-theme="contrast"] {
.MuiPaginationItem-root {
/* color: rgba(0, 0, 0, 0.87); */
color: $contrast-color !important;
}
.MuiButtonBase-root {
/* color: inherit; */
color: $contrast-color !important;
}
.MuiPaginationItem-textPrimary.Mui-selected {
color: $contrast-color !important;
background-color: $contrast-button-background !important;
}
.MuiPaginationItem-page:hover {
background-color: $contrast-button-background !important;
}
.ms-Dropdown-optionText {
color: $ms-color-white !important;
}
[class^="propertyPaneFooter_"] {
background-color: $contrast-background;
}
}
}

View File

@ -0,0 +1,197 @@
import * as React from "react";
import styles from "./News.module.scss";
import { INewsProps } from "./INewsProps";
import { INewsState } from "./INewsState";
import { escape } from "@microsoft/sp-lodash-subset";
import dataservices from "../../services/dataservices";
import { NewsItem } from "./NewsItem/NewsItem";
import { NoNews } from "./NoNews";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { NewsTile } from "./NewsTile/NewsTile";
import * as strings from "NewsWebPartStrings";
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import {
Spinner,
SpinnerSize,
MessageBar,
MessageBarType,
mergeStyles,
getTheme
} from "office-ui-fabric-react";
import { INewsResults } from "../../services/INewsResults";
import { IArticle } from "../../services/IArticle";
import Pagination from "@material-ui/lab/Pagination";
import { Customizer } from "@uifabric/utilities/lib/";
const theme = getTheme();
export default class News extends React.Component<INewsProps, INewsState> {
private _totalResuts: number = 0;
constructor(props: INewsProps) {
super(props);
this.state = {
isLoading: false,
hasError: false,
errorMesage: null,
articles: [],
currentPage: 1,
totalPages: 0
};
}
private _onConfigure = () => {
this.props.context.propertyPane.open();
}
// Component Did Mount
public async componentDidMount(): Promise<void> {
//await dataservices.init(this.props.context);
this._getNews(
this.props.newsUrl,
this.props.apiKey,
this.state.currentPage,
);
}
// Component Did Update
public async componentDidUpdate(
prevProps: INewsProps,
prevState: INewsState
): Promise<void> {
if (
this.props.newsUrl !== prevProps.newsUrl ||
this.props.apiKey !== prevProps.apiKey
) {
this._getNews(
this.props.newsUrl,
this.props.apiKey,
1 // force current page to 1 because new propeerties defined
);
}
}
// Get News from newsApi.org
private _getNews = async (newsUrl: string, apiKey: string, page?: number) => {
try {
const { pageSize } = this.props;
this.setState({ isLoading: true, hasError: false, errorMesage: "" });
const results: any = await dataservices.getNews(newsUrl, apiKey, page);
if (results && results.status == "error") {
throw new Error(results.message);
}
// calculate number of pages
let _reminder: number = (results as INewsResults).totalResults % pageSize; // get Reminder
_reminder = _reminder ? 1 : 0;
const _totalPages: number =
parseInt((results.totalResults / pageSize).toString()) + _reminder;
this.setState({
articles: results ? results.articles : [],
isLoading: false,
hasError: false,
errorMesage: "",
totalPages: _totalPages,
currentPage: page
});
} catch (error) {
console.log("error", error);
this.setState({
isLoading: false,
hasError: true,
errorMesage: error.message
});
}
}
// Render WebPart
public render(): React.ReactElement<INewsProps> {
const { isLoading, errorMesage, articles, hasError } = this.state;
let _renderNews: JSX.Element[] = [];
if (articles && articles.length > 0) {
for (const article of articles) {
if (!this.props.viewOption || this.props.viewOption == "list") {
_renderNews.push(<NewsItem article={article} key={article.title} />);
} else {
_renderNews.push(<NewsTile article={article} key={article.title} />);
}
}
}
return (
<Customizer settings={{ theme: this.props.themeVariant }}>
<div className={styles.News}>
{!this.props.apiKey || !this.props.newsUrl ? (
<Placeholder
iconName="Edit"
iconText={strings.ConfigureWebPartMessage}
description={strings.configureWebPartTextMessage}
buttonLabel={strings.ConfigureWebPartButtonLabel}
onConfigure={this._onConfigure}
/>
) : (
<>
<WebPartTitle
displayMode={this.props.displayMode}
title={this.props.title}
className={styles.title}
updateProperty={this.props.updateProperty}
/>
{isLoading ? (
<Spinner size={SpinnerSize.medium} label="Loading..." />
) : hasError ? (
<>
<MessageBar messageBarType={MessageBarType.error}>
{errorMesage}
</MessageBar>
</>
) : _renderNews.length > 0 ? (
<>
<div
className={
this.props.viewOption == "tiles"
? styles.cardsTiles
: styles.cardsItem
}
>
{_renderNews}
</div>
<div
style={{
display: "flex",
marginTop: 10,
marginBottom: 20,
justifyContent: "center"
}}
>
{this.state.totalPages > 1 && (
<>
<Pagination
color="primary"
count={this.state.totalPages}
page={this.state.currentPage}
onChange={(event, page) => {
this._getNews(
this.props.newsUrl,
this.props.apiKey,
page
);
}}
/>
</>
)}
</div>
</>
) : (
<NoNews />
)}
</>
)}
</div>
</Customizer>
);
}
}

View File

@ -0,0 +1,95 @@
import { IArticle } from "../../../services/IArticle";
import {
Label,
Text,
DocumentCardActivity
} from "office-ui-fabric-react";
import * as moment from "moment";
import styles from "../News.module.scss";
import * as React from "react";
import * as strings from "NewsWebPartStrings";
const _defaultImage:string = require("./../../../../assets/news.jpg");
export const NewsItem = (props: { article: IArticle }) => {
let { article } = props;
return (
<>
<div
className={styles.card}
onClick={ev => {
ev.preventDefault();
window.open(article.url, "_blank");
}}
>
<div
style={{
width:180,
height: '100%'
}}
>
<img
className={styles.image}
src={
article.urlToImage
? article.urlToImage
: _defaultImage
}
width="180"
height="180"
onError={(ev)=> {
ev.currentTarget.setAttribute('src',_defaultImage); // set Default image if can not load article image
}}
></img>
</div>
<div
style={{
width: '100%',
display:'Flex',
height: '100%',
flexDirection: 'column',
justifyContent: 'sflex-start'
}}
>
<Label className={styles.newsTitle} >
{article.title.trim()}
</Label>
<div>
<Text
className={styles.secondaryText}
variant='medium'
block
title={
article.description
? article.description.trim()
: strings.CanNotShowArticleTextMessage
}
>
{article.description
? article.description.trim()
: strings.CanNotShowArticleTextMessage}
</Text>
<DocumentCardActivity
className={styles.author}
activity={moment(article.publishedAt).format("LL")}
people={[
{
name: article.source.name
? article.source.name
: article.author
? article.author
: strings.AuthorNotAvailableMessage,
profileImageSrc: ""
}
]}
/>
</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,178 @@
@import "~office-ui-fabric-react/dist/sass/References.scss";
@media only screen and (min-device-width: 320px) and (max-device-width: 768px) {
.card {
flex-direction: column;
}
.image {
width: 100%;
}
.newsTitle {
margin-top: 20px;
}
}
@media screen and (min-width: 320px) and (max-width: 1024px){
}
.image {
object-fit: cover;
}
.newsTitle {
font-size: 20px;
padding-top: 0px;
/* color: $ms-color-themePrimary; */
margin-bottom: 20px;
@include ms-fontWeight-semibold;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.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;
}
.description {
/* color: $ms-color-neutralSecondaryAlt; */
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
margin-bottom: 10px;
}
.author {
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.date {
@include ms-font-s-plus;
max-width: 200px;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 15px;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
.separator {
border-top: 1px solid lighten(#333, 70%);
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.cardTile {
max-width: 100%
}
[data-theme="default"] {
.cardTile {
border-color: $ms-color-neutralTertiaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralTertiaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
}
[data-theme="dark"] {
.cardTile {
border-color: $ms-color-neutralSecondaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralSecondaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.cardTile:hover {
border-color: $ms-color-neutralTertiary;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
color: $ms-color-neutralTertiaryAlt;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
:global {
.ms-Persona-initials {
color: $ms-color-white;
}
}
}
[data-theme="contrast"] {
.cardTile {
border-color: $ms-color-neutralTertiaryAlt;
}
.separator {
border-top: 1px solid $ms-color-neutralTertiaryAlt;
margin: 0 auto;
padding-bottom: 5px;
width: 90%;
}
.descriptionTile {
/* color: $ms-color-neutralSecondaryAlt;*/
@include ms-fontSize-m;
padding-left: 16px;
padding-right: 16px;
line-height: 22px;
max-height: 135px;
min-height: 135px;
margin-bottom: 15px;
display: block;
overflow: hidden;
color: $ms-color-neutralTertiaryAlt;
font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system,
BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
}
:global {
.ms-Persona-initials {
color: $ms-color-white;
}
}
}

View File

@ -0,0 +1,84 @@
import { IArticle } from "../../../services/IArticle";
import {
Text,
DocumentCard,
DocumentCardTitle,
DocumentCardDetails,
Persona,
DocumentCardActivity,
} from "office-ui-fabric-react";
import * as moment from "moment";
import styles from "./NewsTile.module.scss";
import * as React from "react";
import * as strings from "NewsWebPartStrings";
import { escape } from "@microsoft/sp-lodash-subset";
const _defaultImage: string = require("../../../../assets/news.jpg");
export const NewsTile = (props: { article: IArticle }) => {
let { article } = props;
return (
<>
<DocumentCard
className={styles.cardTile}
onClick={ev => {
ev.preventDefault();
window.open(article.url, "_blank");
}}
>
<div style={{ minWidth: "100%", maxHeight: 160 }}>
<img
className={styles.image}
src={article.urlToImage ? article.urlToImage : _defaultImage}
width={"100%"}
height={160}
onError={ev => {
ev.currentTarget.setAttribute("src", _defaultImage); // set Default image if can not load article image
}}
></img>
</div>
<div
style={{ display: "flex", flexDirection: "column", marginTop: 25 }}
>
<DocumentCardTitle
title={article.title}
shouldTruncate={true}
className={styles.newsTitle}
></DocumentCardTitle>
<DocumentCardDetails styles={{ root: { paddingBottom: 25 } }}>
<Text block variant="mediumPlus" className={styles.descriptionTile}>
{article.description
? article.description
: strings.CanNotShowArticleTextMessage}
</Text>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center"
}}
></div>
</DocumentCardDetails>
<div className={styles.separator}></div>
<DocumentCardActivity
className={styles.author}
activity={moment(article.publishedAt).format("LL")}
people={[
{
name: article.source.name
? article.source.name
: article.author
? article.author
: strings.AuthorNotAvailableMessage,
profileImageSrc: "",
}
] }
/>
</div>
</DocumentCard>
</>
);
};

View File

@ -0,0 +1,31 @@
import {
Icon,
Stack,
DocumentCardTitle,
} from "office-ui-fabric-react";
import styles from "./News.module.scss";
import * as React from "react";
export const NoNews = () => {
return (
<>
<div className="card">
<Stack
style={{ justifyContent: "center", alignItems: "center" }}
verticalAlign="center"
tokens={{ childrenGap: 5 }}
>
<div>
<Icon iconName="News" className={styles.nonewsIcon} />
</div>
<DocumentCardTitle
title="No News find at this moment"
className={styles.nonewsMessage}
></DocumentCardTitle>
</Stack>
</div>
<div className="separator"></div>
</>
);
};

View File

@ -0,0 +1,37 @@
import { IArticle } from "../../../services/IArticle";
import {
Icon,
Label,
Text,
Stack,
Image,
ImageFit,
DocumentCard,
DocumentCardTitle,
DocumentCardDetails
} from "office-ui-fabric-react";
import moment from "moment";
import styles from "../News.module.scss";
import React from "react";
export const NoNews = () => {
return (
<>
<div
className="card"
>
<Stack style={{justifyContent: 'center', alignItems:'center'}} verticalAlign="center" tokens={{ childrenGap: 5 }}>
<div>
<Icon iconName="News" className={styles.nonewsIcon}/>
</div>
<DocumentCardTitle
title="No News find at this moment"
className={styles.nonewsMessage}
></DocumentCardTitle>
</Stack>
</div>
<div className="separator"></div>
</>
);
};

View File

@ -0,0 +1,39 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "137868a8-8e7e-4e06-85f3-a8913f5ff71c",
"alias": "NewsWebPart",
"componentType": "WebPart",
"supportsFullBleed": true, // suport to full width sections
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
"supportsThemeVariants": true,
// 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","TeamsTab","TeamsPersonalApp","SharePointFullPage"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "World News" },
"description": { "default": "World News WebPart" },
"officeFabricIconFontName": "NewsSearch",
"properties": {
"title": "News",
"newsUrl": "https://newsapi.org/v2/everything?q=*",
"apiKey": "3f28c6d88b804e75a66840dbb5aa84b1",
"endpoint":"",
"queryTitleOnly" : "",
"domains": "",
"excludeDomains": "",
"language":"",
"country": "ALL",
"category": "general",
"pagesize": 12,
"viewOption": "tiles" // tiles or list
}
}]
}

View File

@ -0,0 +1,541 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneChoiceGroup,
PropertyPaneToggle,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
PropertyPaneSlider,
PropertyPaneHorizontalRule,
PropertyPaneLabel
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import * as lodash from "lodash";
import * as strings from "NewsWebPartStrings";
import News from "../components/News";
import { INewsProps } from "../components/INewsProps";
import dataservices from "../../services/dataservices";
import { DisplayMode } from "@microsoft/sp-core-library";
import * as $ from "jquery";
import { loadTheme, IDropdownOption } from "office-ui-fabric-react";
import { PropertyFieldMultiSelect } from "@pnp/spfx-property-controls/lib/PropertyFieldMultiSelect";
import { ISourcesResults } from "../../services/ISourcesResults";
import { ISource } from "../../services/ISource";
import { ThemeProvider, ThemeChangedEventArgs, IReadonlyTheme } from '@microsoft/sp-component-base';
const languages: IPropertyPaneDropdownOption[] = [
{ key: "all", text: "All Languages" },
{ key: "ar", text: "Arabic" },
{ key: "de", text: "German" },
{ key: "en", text: "English" },
{ key: "es", text: "Castilian" },
{ key: "fr", text: "French" },
{ key: "he", text: "Hebrew" },
{ key: "it", text: "Italian" },
{ key: "nl", text: "Dutch" },
{ key: "no", text: "Norwegian" },
{ key: "pt", text: "Portuguese" },
{ key: "ru", text: "Russian" },
{ key: "se", text: "Northern Sami" },
{ key: "zh", text: "Chinese" }
];
const countries: IPropertyPaneDropdownOption[] = [
{ key: "ALL", text: "All Countries" },
{ key: "AE", text: "United Arab Emirates" },
{ key: "AR", text: "Argentina" },
{ key: "AT", text: "Austria" },
{ key: "AU", text: "Australia" },
{ key: "BE", text: "Belgium" },
{ key: "BG", text: "Bulgaria" },
{ key: "BR", text: "Brazil" },
{ key: "CA", text: "Canada" },
{ key: "CH", text: "Switzerland" },
{ key: "CN", text: "China" },
{ key: "CO", text: "Colombia" },
{ key: "CU", text: "Cuba" },
{ key: "CZ", text: "Czech Republic" },
{ key: "DE", text: "Germany" },
{ key: "EG", text: "Egypt" },
{ key: "FR", text: "France" },
{ key: "GB", text: "United Kingdom" },
{ key: "GR", text: "Greece" },
{ key: "HK", text: "Hong Kong" },
{ key: "HU", text: "Hungary" },
{ key: "ID", text: "Indonesia" },
{ key: "IE", text: "Ireland" },
{ key: "IL", text: "Israel" },
{ key: "IN", text: "India" },
{ key: "IT", text: "Italy" },
{ key: "JP", text: "Japan" },
{ key: "KR", text: "Korea, Republic of" },
{ key: "LT", text: "Lithuania" },
{ key: "LV", text: "Latvia" },
{ key: "MA", text: "Morocco" },
{ key: "MX", text: "Mexico" },
{ key: "MY", text: "Malaysia" },
{ key: "NG", text: "Nigeria" },
{ key: "NL", text: "Netherlands" },
{ key: "NO", text: "Norway" },
{ key: "NZ", text: "New Zealand" },
{ key: "PH", text: "Philippines" },
{ key: "PL", text: "Poland" },
{ key: "PT", text: "Portugal" },
{ key: "RO", text: "Romania" },
{ key: "RS", text: "Serbia" },
{ key: "RU", text: "Russian Federation" },
{ key: "SA", text: "Saudi Arabia" },
{ key: "SE", text: "Sweden" },
{ key: "SG", text: "Singapore" },
{ key: "SI", text: "Slovenia" },
{ key: "SK", text: "Slovakia" },
{ key: "TH", text: "Thailand" },
{ key: "TR", text: "Turkey" },
{ key: "TW", text: "Taiwan, Province of China" },
{ key: "UA", text: "Ukraine" },
{ key: "US", text: "United States" },
{ key: "VE", text: "Venezuela, Bolivarian Republic of" },
{ key: "ZA", text: "South Africa" }
];
const categories: IPropertyPaneDropdownOption[] = [
{ key: "business", text: "business" },
{ key: "entertainment", text: "entertainment" },
{ key: "general", text: "general" },
{ key: "health", text: "health" },
{ key: "science", text: "science" },
{ key: "sports", text: "sports" },
{ key: "technology", text: "technology" }
];
const teamsDefaultTheme = require("../../common/TeamsDefaultTheme.json");
const teamsDarkTheme = require("../../common/TeamsDarkTheme.json");
const teamsContrastTheme = require("../../common/TeamsContrastTheme.json");
export interface INewsWebPartProps {
title: string;
newsUrl: string;
apiKey: string;
endpoint: number;
queryTitleOnly: boolean;
domains: string;
excludeDomains: string;
language: string;
country: string;
category: string;
query: string;
pagesize: number;
viewOption: string;
sources: string[];
}
export default class NewsWebPart extends BaseClientSideWebPart<
INewsWebPartProps
> {
private _sourcesOptions: IDropdownOption[] = [];
private _themeProvider: ThemeProvider;
private _themeVariant: IReadonlyTheme | undefined;
protected async onInit<T>(): Promise<T> {
await dataservices.init(this.context);
this._themeProvider = this.context.serviceScope.consume(ThemeProvider.serviceKey);
// If it exists, get the theme variant
this._themeVariant = this._themeProvider.tryGetTheme();
// Register a handler to be notified if the theme variant changes
this._themeProvider.themeChangedEvent.add(this, this._handleThemeChangedEvent);
// test if is teams context
if (this.context.sdks.microsoftTeams) {
// in teams ?
const context = this.context.sdks.microsoftTeams!.context;
this._applyTheme(context.theme || "default");
this.context.sdks.microsoftTeams.teamsJs.registerOnThemeChangeHandler(
this._applyTheme
);
}
return Promise.resolve();
}
/**
* Update the current theme variant reference and re-render.
*
* @param args The new theme
*/
private _handleThemeChangedEvent(args: ThemeChangedEventArgs): void {
this._themeVariant = args.theme;
this.render();
}
// Apply Teams Context
private _applyTheme = (theme: string): void => {
this.context.domElement.setAttribute("data-theme", theme);
document.body.setAttribute("data-theme", theme);
if (theme == "dark") {
loadTheme({
palette: teamsDarkTheme
});
}
if (theme == "default") {
loadTheme({
palette: teamsDefaultTheme
});
}
if (theme == "contrast") {
loadTheme({
palette: teamsContrastTheme
});
}
}
private _validateNewsUrl = (value: string): string => {
try {
let _url = new URL(value);
return "";
} catch (error) {
return "news Url is not valid, please specify valid url";
}
}
public updateProperty = (value: string) => {
this.properties.title = value;
}
private _getSources = async (apiKey: string) => {
let _resultSources: ISourcesResults = await dataservices.getSources(apiKey);
try {
if (_resultSources && _resultSources.sources.length > 0) {
for (const source of _resultSources.sources) {
this._sourcesOptions.push({ key: source.id, text: source.name });
}
return;
}
} catch (error) {
console.log("Error loading Sources", error);
return;
}
}
// Render WebPart
public render(): void {
const element: React.ReactElement<INewsProps> = React.createElement(
News,
{
newsUrl: this.properties.newsUrl,
apiKey: this.properties.apiKey,
context: this.context,
title: this.properties.title,
updateProperty: this.updateProperty,
displayMode: this.displayMode,
viewOption: this.properties.viewOption,
pageSize: this.properties.pagesize,
themeVariant: this._themeVariant,
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected async onPropertyPaneConfigurationStart() {
await this._getSources(this.properties.apiKey);
this.context.propertyPane.refresh();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
let _showPropertyQueryTitle: any = "";
let _showPropertyDomains: any = "";
let _showPropertyexcludeDomains: any = "";
let _showPropertyLanguage: any = "";
let _showPropertyCountry: any = "";
let _showPropertyCategory: any = "";
let _viewOption: string = undefined;
if (!this.properties.viewOption || this.properties.viewOption === "list") {
_viewOption = "list";
} else {
_viewOption = "tiles";
}
if (!this.properties.newsUrl) {
this.properties.newsUrl =
"https://newsapi.org/v2/top-headlines?category=general&sortBy=publishedAt";
}
if (!this.properties.endpoint) {
this.properties.endpoint = 1;
}
if (!this.properties.category) {
this.properties.category = "general";
}
// teste if All News or TOP Headding news
switch (this.properties.endpoint) {
case 2:
this.properties.country = "";
this.properties.category = "";
_showPropertyCountry = "";
_showPropertyCategory = "";
_showPropertyQueryTitle = PropertyPaneToggle("queryTitleOnly", {
label: "Search on Article Title Only",
onText: "On",
offText: "Off",
checked: this.properties.queryTitleOnly
});
_showPropertyDomains = PropertyPaneTextField("domains", {
label: "Selected Domains",
value: this.properties.domains,
description:
"comma-seperated (eg bbc.co.uk, techcrunch.com, engadget.com)"
});
_showPropertyexcludeDomains = PropertyPaneTextField("excludeDomains", {
label: "Exclude Domains",
value: this.properties.excludeDomains,
description:
"comma-seperated (eg bbc.co.uk, techcrunch.com, engadget.com)"
});
// Create a news URL API
this.properties.newsUrl = `https://newsapi.org/v2/everything?sortBy=publishedAt`;
// Test Properties
// Query on Article Title
if (this.properties.queryTitleOnly) {
if (
this.properties.query &&
this.properties.query.trim().length > 0
) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&qInTitle=${_query}`;
} else {
this.properties.newsUrl = `${this.properties.newsUrl}&qInTitle=*`;
}
}
// Domains
if (this.properties.domains) {
this.properties.newsUrl = `${this.properties.newsUrl}&domains=${this.properties.domains}`;
}
// Excluded Domains
if (this.properties.excludeDomains) {
this.properties.newsUrl = `${this.properties.newsUrl}&excludeDomains=${this.properties.excludeDomains}`;
}
// Language
if (this.properties.language && this.properties.language !== "all") {
this.properties.newsUrl = `${this.properties.newsUrl}&language=${this.properties.language}`;
}
// Query Title Only
if (!this.properties.queryTitleOnly) {
if (
this.properties.query &&
this.properties.query.trim().length > 0
) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&q=${_query}`;
} else {
this.properties.newsUrl = `${this.properties.newsUrl}&q=*`;
}
}
if (this.properties.pagesize) {
this.properties.newsUrl = `${this.properties.newsUrl}&pageSize=${this.properties.pagesize}`;
}
if (this.properties.sources && this.properties.sources.length > 0) {
if (this.properties.sources.length < 20) {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.join()}`;
} else {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.slice(0, 19).join()}`;
}
}
_showPropertyLanguage = PropertyPaneDropdown("language", {
label: "Show Articles in this language",
options: lodash.sortBy(languages, ["key"]),
selectedKey: this.properties.language || "all"
});
break;
// Top Heading
case 1:
// Reset Properties Vars
this.properties.queryTitleOnly = false;
this.properties.domains = "";
this.properties.excludeDomains = "";
this.properties.language = "";
_showPropertyQueryTitle = "";
_showPropertyDomains = "";
_showPropertyexcludeDomains = "";
_showPropertyLanguage = "";
this.properties.newsUrl = `https://newsapi.org/v2/top-headlines?sortBy=publishedAt`;
if (this.properties.query && this.properties.query.trim().length > 0) {
let _query: string = encodeURIComponent(this.properties.query);
this.properties.newsUrl = `${this.properties.newsUrl}&q=${_query}`;
}
if (this.properties.pagesize) {
this.properties.newsUrl = `${this.properties.newsUrl}&pageSize=${this.properties.pagesize}`;
}
// Has sources ? add parameter to newsURl disable country and Category
if (this.properties.sources && this.properties.sources.length > 0) {
if (this.properties.sources.length < 20) {
// only the first 20 sources selectd limited by API
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.join()}`;
} else {
this.properties.newsUrl = `${
this.properties.newsUrl
}&sources=${this.properties.sources.slice(0, 19).join()}`;
}
} else {
// Show Category and Country if sources is not specified
if (this.properties.category) {
this.properties.newsUrl = `${this.properties.newsUrl}&category=${this.properties.category}`;
}
if (this.properties.country && this.properties.country !== "ALL") {
this.properties.newsUrl = `${this.properties.newsUrl}&country=${this.properties.country}`;
}
_showPropertyCountry = PropertyPaneDropdown("country", {
label: "Country",
options: lodash.sortBy(countries, ["text"]),
selectedKey: this.properties.country || "ALL"
});
_showPropertyCategory = PropertyPaneDropdown("category", {
label: "Category",
options: lodash.sortBy(categories, ["key"]),
selectedKey: this.properties.category || "general"
});
}
break;
default:
break;
}
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("title", {
label: strings.Title,
value: this.properties.title
}),
PropertyPaneTextField("query", {
label: "Search Keyword or Phrase",
value: this.properties.query
}),
PropertyFieldMultiSelect("sources", {
key: "sources",
label: "Sources",
disabled: false,
options: this._sourcesOptions,
selectedKeys: this.properties.sources
}),
PropertyPaneChoiceGroup("endpoint", {
label: "Show Articles from:",
options: [
{ text: "Top Headlines", key: 1 },
{ text: "All News", key: 2 }
]
}),
_showPropertyCountry,
_showPropertyCategory,
_showPropertyQueryTitle,
_showPropertyDomains,
_showPropertyexcludeDomains,
_showPropertyLanguage
]
}
]
},
{
header: {
description: strings.ViewSettings
},
groups: [
{
groupFields: [
PropertyPaneChoiceGroup("viewOption", {
label: strings.ViewOption,
options: [
{
text: "List View",
key: "list",
checked: _viewOption === "list" ? true : false,
iconProps: { officeFabricIconFontName: "list" }
},
{
text: "Tiles View",
key: "tiles",
checked: _viewOption === "ltiles" ? true : false,
iconProps: { officeFabricIconFontName: "Tiles" }
}
]
}),
PropertyPaneLabel("", { text: "" }),
PropertyPaneSlider("pagesize", {
label: strings.PageSizeLabel,
max: 100,
min: 3,
step: 1,
showValue: true,
value: this.properties.pagesize
}),
PropertyPaneLabel("", { text: strings.APILabelText }),
PropertyPaneTextField("apiKey", {
label: strings.ApiKey,
value: this.properties.apiKey,
validateOnFocusOut: true,
onGetErrorMessage: value => {
if (!value) {
return "ApiKey is Required";
} else {
return "";
}
}
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,18 @@
define([], function() {
return {
ConfigureWebPartButtonLabel: "Configure",
configureWebPartTextMessage: "Please configure the web part.",
ConfigureWebPartMessage: "Configure News web part",
AuthorNotAvailableMessage: "Author not Available",
CanNotShowArticleTextMessage: "Can not show article text, please click to see the full article.",
APILabelText: "APIKEY use on API Calls, Please go to https://newsapi.org see more details",
PageSizeLabel: "Number of Articles per page",
ViewOption: "View Option",
ViewSettings: "View Settings",
Title: "Title",
"PropertyPaneDescription": "This Web Part Get News from newsapi.org API, please go to https://newsapi.org for more information.",
"BasicGroupName": "News",
"NewsUrlFieldLabel": "NewsApi Url",
"ApiKey": "APIKEY"
}
});

View File

@ -0,0 +1,21 @@
declare interface INewsWebPartStrings {
ConfigureWebPartButtonLabel: string;
configureWebPartTextMessage: string;
ConfigureWebPartMessage: string;
AuthorNotAvailableMessage: string;
CanNotShowArticleTextMessage: string;
APILabelText: string;
PageSizeLabel: string;
ViewOption: string;
ViewSettings: string;
Title: string;
PropertyPaneDescription: string;
BasicGroupName: string;
NewsUrlFieldLabel: string;
ApiKey: string;
}
declare module 'NewsWebPartStrings' {
const strings: INewsWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 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
}
}

File diff suppressed because it is too large Load Diff

View File

@ -121,7 +121,7 @@ export function ManageApps(appProps: IManageAppsProps) {
},
{
title: "Url",
field: "Url",
field: "url",
editComponent: props => (
<TextField
underlined
@ -279,6 +279,7 @@ export function ManageApps(appProps: IManageAppsProps) {
showTitle:false,
searchFieldAlignment:'left',
pageSize: 7,
pageSizeOptions: [],
search: true,
minBodyHeight: "100%"
}}