Extract functions

This commit is contained in:
AriGunawan 2022-12-02 01:59:43 +10:00 committed by Hugo Bernier
parent 79666792a1
commit 4921e3b028
11 changed files with 276 additions and 109 deletions

View File

@ -79,7 +79,7 @@ module.exports = {
// This rule should be suppressed only in very special cases such as JSON.stringify()
// where the type really can be anything. Even if the type is flexible, another type
// may be more appropriate such as "unknown", "{}", or "Record<k,V>".
'@typescript-eslint/no-explicit-any': 1,
'@typescript-eslint/no-explicit-any': 0,
// RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch()
// handler. Thus wherever a Promise arises, the code must either append a catch handler,
// or else return the object to a caller (who assumes this responsibility). Unterminated

View File

@ -1,9 +1,9 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-birthdays-per-month-client-side-solution",
"name": "React Birthdays Per Month",
"id": "e4cd97a3-4515-42cd-8a12-f765eaf60caa",
"version": "1.0.0.0",
"version": "1.1.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
@ -35,6 +35,6 @@
]
},
"paths": {
"zippedPackage": "solution/react-birthdays-per-month.sppkg"
"zippedPackage": "solution/React Birthdays Per Month.sppkg"
}
}

View File

@ -15,6 +15,8 @@
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
"@microsoft/sp-property-pane": "1.15.2",
"@microsoft/sp-webpart-base": "1.15.2",
"@pnp/sp": "^3.8.0",
"date-fns": "^2.29.3",
"office-ui-fabric-react": "7.185.7",
"react": "16.13.1",
"react-dom": "16.13.1",

View File

@ -11,12 +11,14 @@ specifiers:
'@microsoft/sp-office-ui-fabric-core': 1.15.2
'@microsoft/sp-property-pane': 1.15.2
'@microsoft/sp-webpart-base': 1.15.2
'@pnp/sp': ^3.8.0
'@rushstack/eslint-config': 2.5.1
'@types/react': 16.9.51
'@types/react-dom': 16.9.8
'@types/webpack-env': ~1.15.2
ajv: ^6.12.5
autoprefixer: ^10.4.13
date-fns: ^2.29.3
eslint-plugin-react-hooks: 4.3.0
gulp: 4.0.2
gulp-postcss: ^9.0.1
@ -36,6 +38,8 @@ dependencies:
'@microsoft/sp-office-ui-fabric-core': 1.15.2
'@microsoft/sp-property-pane': 1.15.2_7ombvvupg4tnmt4iqt5m47i6cu
'@microsoft/sp-webpart-base': 1.15.2_7ombvvupg4tnmt4iqt5m47i6cu
'@pnp/sp': 3.8.0
date-fns: 2.29.3
office-ui-fabric-react: 7.185.7_24igt2r6uynb67fv3burekl4py
react: 16.13.1
react-dom: 16.13.1_react@16.13.1
@ -76,7 +80,7 @@ packages:
resolution: {integrity: sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==}
engines: {node: '>=12.0.0'}
dependencies:
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/core-asynciterator-polyfill/1.0.2:
@ -89,7 +93,7 @@ packages:
engines: {node: '>=12.0.0'}
dependencies:
'@azure/abort-controller': 1.1.0
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/core-http/1.2.6:
@ -107,7 +111,7 @@ packages:
node-fetch: 2.6.7
process: 0.11.10
tough-cookie: 4.1.2
tslib: 2.3.1
tslib: 2.4.0
tunnel: 0.0.6
uuid: 8.3.2
xml2js: 0.4.23
@ -123,7 +127,7 @@ packages:
'@azure/core-http': 1.2.6
'@azure/core-tracing': 1.0.0-preview.11
events: 3.3.0
tslib: 2.3.1
tslib: 2.4.0
transitivePeerDependencies:
- encoding
dev: true
@ -132,7 +136,7 @@ packages:
resolution: {integrity: sha512-H6Tg9eBm0brHqLy0OSAGzxIh1t4UL8eZVrSUMJ60Ra9cwq2pOskFqVpz2pYoHDsBY1jZ4V/P8LRGb5D5pmC6rg==}
engines: {node: '>=12.0.0'}
dependencies:
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/core-tracing/1.0.0-preview.11:
@ -141,7 +145,7 @@ packages:
dependencies:
'@opencensus/web-types': 0.0.7
'@opentelemetry/api': 1.0.0-rc.0
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/core-tracing/1.0.0-preview.7:
@ -158,7 +162,7 @@ packages:
dependencies:
'@opencensus/web-types': 0.0.7
'@opentelemetry/api': 0.10.2
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/identity/1.0.3:
@ -182,7 +186,7 @@ packages:
resolution: {integrity: sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==}
engines: {node: '>=12.0.0'}
dependencies:
tslib: 2.3.1
tslib: 2.4.0
dev: true
/@azure/msal-browser/2.22.0:
@ -208,7 +212,7 @@ packages:
'@azure/logger': 1.0.3
'@opentelemetry/api': 0.10.2
events: 3.3.0
tslib: 2.3.1
tslib: 2.4.0
transitivePeerDependencies:
- encoding
dev: true
@ -225,7 +229,7 @@ packages:
'@azure/logger': 1.0.3
'@opentelemetry/api': 0.10.2
events: 3.3.0
tslib: 2.3.1
tslib: 2.4.0
transitivePeerDependencies:
- encoding
dev: true
@ -1126,7 +1130,7 @@ packages:
dependencies:
'@azure/msal-browser': 2.22.0
'@babel/runtime': 7.20.0
tslib: 2.3.1
tslib: 2.4.0
dev: false
/@microsoft/office-ui-fabric-react-bundle/1.15.2_7ombvvupg4tnmt4iqt5m47i6cu:
@ -1790,6 +1794,31 @@ packages:
webpack-dev-server: 3.11.2_jtacdaqalodjhudd6qhs6dld5q
dev: true
/@pnp/core/3.8.0:
resolution: {integrity: sha512-xPUohlAxbLate6yjR0+ajE29PJouhLqKW6iO37Xl3npT5IdC3C8k/W/RTvXEu1xegHAM+LVYQ3/OhKr+zxf6tw==}
engines: {node: '>=14.15.1'}
dependencies:
tslib: 2.4.0
dev: false
/@pnp/queryable/3.8.0:
resolution: {integrity: sha512-W2pX05FmIuFaQTNqcbrblgFjd9WVc0W4ElKAf5u4vMp0zlPidtsybkME6YC0o4k9+4LgTT9HZzCEY3ovuxhZ4g==}
engines: {node: '>=14.15.1'}
dependencies:
'@pnp/core': 3.8.0
tslib: 2.4.0
dev: false
/@pnp/sp/3.8.0:
resolution: {integrity: sha512-8mfvaCODbdfud3lJVRWbVWIQ4SlbN3sNBhSEDq4fHnC/SNSn21V2ADOR//LqYcovdPQU3AKTlqrorItoSnK8/g==}
engines: {node: '>=14.15.1'}
requiresBuild: true
dependencies:
'@pnp/core': 3.8.0
'@pnp/queryable': 3.8.0
tslib: 2.4.0
dev: false
/@pnpm/error/1.4.0:
resolution: {integrity: sha512-vxkRrkneBPVmP23kyjnYwVOtipwlSl6UfL+h+Xa3TrABJTz5rYBXemlTsU5BzST8U4pD7YDkTb3SQu+MMuIDKA==}
engines: {node: '>=10.16'}
@ -5490,6 +5519,11 @@ packages:
whatwg-url: 7.1.0
dev: true
/date-fns/2.29.3:
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
engines: {node: '>=0.11'}
dev: false
/dateformat/1.0.12:
resolution: {integrity: sha512-5sFRfAAmbHdIts+eKjR9kYJoF0ViCMVX9yqLu5A7S/v+nd077KgCITOMiirmyCBiZpKLDXbBOkYm6tu7rX/TKg==}
hasBin: true
@ -14185,6 +14219,10 @@ packages:
/tslib/2.3.1:
resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==}
dev: false
/tslib/2.4.0:
resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==}
/tsutils/3.21.0_typescript@4.5.5:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}

View File

@ -0,0 +1,6 @@
import { User } from "./User";
export interface BirthdaysInMonth {
title: string;
users: Array<User>;
}

View File

@ -0,0 +1,8 @@
export interface User {
id: number;
name: string;
email: string;
month: string;
monthIndex: number;
date: number;
}

View File

@ -0,0 +1,90 @@
import { SPFI } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import { BirthdaysInMonth } from "../models/BirthdaysInMonth";
import { User } from "../models/User";
import { sortBy } from "@microsoft/sp-lodash-subset";
export class SharePointService {
private readonly _spfi: SPFI;
constructor(spfi: SPFI) {
this._spfi = spfi;
}
public async GetBirthdays(): Promise<Array<BirthdaysInMonth>> {
const items: Array<any> = await this._spfi.web.lists
.getByTitle("Birthdays")
.items.expand("Employee")
.select("ID,Month,Date,Employee/Title,Employee/UserName")();
return this.ProcessData(items);
}
private GenerateMonths(): Array<BirthdaysInMonth> {
const months: Array<BirthdaysInMonth> = [];
for (let i = 0; i < 12; i++) {
const today = new Date();
today.setMonth(today.getMonth() + i);
months.push({
title: today.toLocaleString("en-AU", { month: "long" }),
users: [],
});
}
return months;
}
private GetMonthIndex(month: string): number {
switch (month) {
case "January":
return 0;
case "February":
return 1;
case "March":
return 2;
case "April":
return 3;
case "May":
return 4;
case "June":
return 5;
case "July":
return 6;
case "August":
return 7;
case "September":
return 8;
case "October":
return 9;
case "November":
return 10;
case "December":
return 11;
}
}
private ProcessData(items: any): Array<BirthdaysInMonth> {
const months = this.GenerateMonths();
for (let i = 0; i < months.length; i++) {
const month = months[i];
month.users = sortBy(
items
.filter((item: any) => item.Month === month.title)
.map(
(item: any): User => ({
id: item.ID,
name: item.Employee.Title,
email: item.Employee.UserName,
date: item.Date,
month: item.Month,
monthIndex: this.GetMonthIndex(item.Month),
})
),
"date"
);
}
return months;
}
}

View File

@ -18,11 +18,10 @@
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced
"group": { "default": "Advanced" },
"title": { "default": "BirthdaysPerMonth" },
"description": { "default": "BirthdaysPerMonth description" },
"officeFabricIconFontName": "Page",
"title": { "default": "Birthdays Per Month" },
"description": { "default": "" },
"officeFabricIconFontName": "BirthdayCake",
"properties": {
"description": "BirthdaysPerMonth"
}
}]
}

View File

@ -6,6 +6,7 @@ import {
PropertyPaneTextField,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { spfi, SPFx, SPFI } from "@pnp/sp";
import * as strings from "BirthdaysPerMonthWebPartStrings";
import "../../../assets/dist/tailwind.css";
@ -13,29 +14,31 @@ import {
BirthdaysPerMonth,
IBirthdaysPerMonthProps,
} from "./components/BirthdaysPerMonth";
import { BirthdaysInMonth } from "../../models/BirthdaysInMonth";
import { SharePointService } from "../../utils/SharePointService";
export interface IBirthdaysPerMonthWebPartProps {
description: string;
}
export default class BirthdaysPerMonthWebPart extends BaseClientSideWebPart<IBirthdaysPerMonthWebPartProps> {
private _isDarkTheme: boolean = false;
private _environmentMessage: string = "";
private _spfi: SPFI;
public render(): void {
public async render(): Promise<void> {
const sharePointService = new SharePointService(this._spfi);
const birthdays: Array<BirthdaysInMonth> =
await sharePointService.GetBirthdays();
const elementProps: IBirthdaysPerMonthProps = {
data: birthdays,
};
const element: React.ReactElement<IBirthdaysPerMonthProps> =
React.createElement(BirthdaysPerMonth, {
description: this.properties.description,
isDarkTheme: this._isDarkTheme,
environmentMessage: this._environmentMessage,
hasTeamsContext: !!this.context.sdks.microsoftTeams,
userDisplayName: this.context.pageContext.user.displayName,
});
React.createElement(BirthdaysPerMonth, elementProps);
ReactDom.render(element, this.domElement);
}
protected onInit(): Promise<void> {
this._spfi = spfi().using(SPFx(this.context));
return super.onInit();
}

View File

@ -1,87 +1,17 @@
import * as React from "react";
import styles from "./BirthdaysPerMonth.module.scss";
import { escape } from "@microsoft/sp-lodash-subset";
import { BirthdaysInMonth } from "../../../models/BirthdaysInMonth";
import { MonthSection } from "./MonthSection";
interface IBirthdaysPerMonthProps {}
interface IBirthdaysPerMonthProps {
data: Array<BirthdaysInMonth>;
}
const BirthdaysPerMonth = () => {
const BirthdaysPerMonth = (props: IBirthdaysPerMonthProps): JSX.Element => {
return (
<section className={`tw-text-red-700`}>
<div className={styles.welcome}>
<img
alt=""
src={require("../assets/welcome-light.png")}
className={styles.welcomeImage}
/>
<h2>Well done, Ari!</h2>
</div>
<div>
<h3>Welcome to SharePoint Framework!</h3>
<p>
The SharePoint Framework (SPFx) is a extensibility model for Microsoft
Viva, Microsoft Teams and SharePoint. It&#39;s the easiest way to
extend Microsoft 365 with automatic Single Sign On, automatic hosting
and industry standard tooling.
</p>
<h4>Learn more about SPFx development:</h4>
<ul className={styles.links}>
<li>
<a href="https://aka.ms/spfx" target="_blank" rel="noreferrer">
SharePoint Framework Overview
</a>
</li>
<li>
<a
href="https://aka.ms/spfx-yeoman-graph"
target="_blank"
rel="noreferrer"
>
Use Microsoft Graph in your solution
</a>
</li>
<li>
<a
href="https://aka.ms/spfx-yeoman-teams"
target="_blank"
rel="noreferrer"
>
Build for Microsoft Teams using SharePoint Framework
</a>
</li>
<li>
<a
href="https://aka.ms/spfx-yeoman-viva"
target="_blank"
rel="noreferrer"
>
Build for Microsoft Viva Connections using SharePoint Framework
</a>
</li>
<li>
<a
href="https://aka.ms/spfx-yeoman-store"
target="_blank"
rel="noreferrer"
>
Publish SharePoint Framework applications to the marketplace
</a>
</li>
<li>
<a
href="https://aka.ms/spfx-yeoman-api"
target="_blank"
rel="noreferrer"
>
SharePoint Framework API reference
</a>
</li>
<li>
<a href="https://aka.ms/m365pnp" target="_blank" rel="noreferrer">
Microsoft 365 Developer Community
</a>
</li>
</ul>
</div>
<section>
{props.data.map((month, index) => (
<MonthSection key={month.title} data={month} index={index} />
))}
</section>
);
};

View File

@ -0,0 +1,91 @@
import { addYears, format } from "date-fns/esm";
import formatDistance from "date-fns/formatDistance";
import set from "date-fns/set";
import { Icon, IconButton } from "office-ui-fabric-react";
import * as React from "react";
import { BirthdaysInMonth } from "../../../models/BirthdaysInMonth";
interface IMonthSectionProps {
data: BirthdaysInMonth;
index: number;
}
const MonthSection = (props: IMonthSectionProps): JSX.Element => {
const [isExpand, toggleExpand] = React.useReducer(
(previous) => !previous,
props.index === 0 || props.index === 1
);
const generateYearLabel = (): string => {
if (props.data.title !== "January" || props.index === 0) return "";
const today = new Date();
today.setFullYear(today.getFullYear() + 1);
return " " + today.getFullYear();
};
const getDate = (date: number, month: number): Date => {
let birthdateDate = set(new Date(), { month, date });
if (birthdateDate.getMonth() < new Date().getMonth()) {
birthdateDate = addYears(birthdateDate, 1);
}
return birthdateDate;
};
const generateDateLabel = (date: number, month: number): string => {
const birthdateDate = getDate(date, month);
return format(birthdateDate, "E, d MMM");
};
const generateDistanceLabel = (date: number, month: number): string => {
const birthdateDate = getDate(date, month);
return formatDistance(birthdateDate, new Date(), { addSuffix: true });
};
return (
<div onClick={toggleExpand}>
<div className="hover:tw-cursor-pointer tw-p-3 tw-bg-[#f3f2f1] tw-font-semibold tw-text-lg tw-flex tw-gap-2">
<Icon
className={`${isExpand ? "tw-rotate-90" : ""} tw-transition-all`}
iconName="ChevronRightMed"
/>
<div>
{props.data.title}
{generateYearLabel()}
</div>
</div>
{isExpand && (
<div className="tw-flex tw-gap-8 tw-p-3 tw-pl-[2.35rem] tw-border tw-border-solid tw-border-[#f3f2f1]">
{props.data.users.length === 0 && <div>No birthdays this month.</div>}
{props.data.users.map((user) => (
<div className="tw-flex tw-gap-4" key={user.id}>
<div>
<img
className="tw-rounded-full tw-w-20"
src={`/_layouts/15/userphoto.aspx?UserName=${user.email}`}
/>
</div>
<div className="tw-flex tw-flex-col tw-justify-center">
<div className="tw-text-lg tw-font-semibold tw-flex tw-gap-1 tw-items-center">
<div>{user.name}</div>
<IconButton
iconProps={{ iconName: "Mail" }}
title="Mail"
onClick={(event) => {
event.stopPropagation();
window.open(`mailto:${user.email}`);
}}
/>
</div>
<div>{generateDateLabel(user.date, user.monthIndex)}</div>
<div>{generateDistanceLabel(user.date, user.monthIndex)}</div>
</div>
</div>
))}
</div>
)}
</div>
);
};
export { MonthSection };