Merge pull request #3028 from srpmtt/react-feedback-sidebar

react-feedback-sidebar sample
This commit is contained in:
Hugo Bernier 2022-11-06 23:10:26 -05:00 committed by GitHub
commit 0c922857be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 27413 additions and 2 deletions

View File

@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
release
solution
temp
*.sppkg
.heft
# 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,16 @@
!dist
config
gulpfile.js
release
src
temp
tsconfig.json
tslint.json
*.log
.yo-rc.json
.vscode

View File

@ -0,0 +1,16 @@
{
"@microsoft/generator-sharepoint": {
"plusBeta": false,
"isCreatingSolution": true,
"version": "1.15.2",
"libraryName": "feedback-webpart",
"libraryId": "a526cb38-54b1-4704-bb86-295189772026",
"environment": "spo",
"packageManager": "npm",
"solutionName": "feedback-webpart",
"solutionShortDescription": "feedback-webpart description",
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,7 @@
Copyright 2022 srpmtt
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,69 @@
# Feedback Sidebar Web Part
## Summary
This web part displays a sidebar that allows users to provide feedback on various sections of your site.
The sections displayed in the sidebar are dynamically generated based on the web parts configured on the site page.
Place this web part on top of your page in order to show the Feedback button.
![Feedback Sidebar Web Part](./assets/preview-img-01.png)
![Feedback Sidebar Web Part](./assets/preview.gif)
## Compatibility
![SPFx 1.15.0](https://img.shields.io/badge/SPFx-1.15.0-green.svg)
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
![Node.js v16 | v14 | v12 ](https://img.shields.io/badge/Node.js-v16%20%7C%20v14%20%7C%20v12-green.svg)
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
![Does not work with SharePoint 2016 (Feature Pack 2)](https://img.shields.io/badge/SharePoint%20Server%202016%20(Feature%20Pack%202)-Incompatible-red.svg "SharePoint Server 2016 Feature Pack 2 requires SPFx 1.1")
![Local Workbench Unsupported](https://img.shields.io/badge/Local%20Workbench-Unsupported-red.svg "Local workbench is no longer available as of SPFx 1.13 and above")
![Hosted Workbench Compatible](https://img.shields.io/badge/Hosted%20Workbench-Compatible-green.svg)
![Compatible with Remote Containers](https://img.shields.io/badge/Remote%20Containers-Compatible-green.svg)
## Prerequisites
- Office 365 subscription with SharePoint Online
- SharePoint Framework [development environment](https://docs.microsoft.com/sharepoint/dev/spfx/set-up-your-development-environment) set up
## Minimal Path to Awesome
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-feedback-sidebar) then unzip it)
- From your command line, change your current directory to the directory containing this sample (`react-feedback-sidebar`, located under `samples`)
- create the package:
- `npm i`
- `gulp bundle --ship`
- `gulp package-solution --ship`
- Add the app package to Site Collection App Catalog and install the App
- Add the web part to a page
## Solution
| Solution | Author(s) |
| ---------------------- | --------------------------------------------------------- |
| react-feedback-sidebar | [Alessia De Martino](https://github.com/AlessiaDeMartino) |
| react-feedback-sidebar | Andrea Bellini |
| react-feedback-sidebar | [Matteo Serpi](https://github.com/srpmtt) |
| react-feedback-sidebar | [Michele Catena](https://github.com/10xMike) |
## Help
We do not support samples, but we this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.
If you're having issues building the solution, please run [spfx doctor](https://pnp.github.io/cli-microsoft365/cmd/spfx/spfx-doctor/) from within the solution folder to diagnose incompatibility issues with your environment.
You can try looking at [issues related to this sample](https://github.com/pnp/sp-dev-fx-webparts/issues?q=label%3A%22sample%3A%20react-feedback-sidebar") to see if anybody else is having the same issues.
You can also try looking at [discussions related to this sample](https://github.com/pnp/sp-dev-fx-webparts/discussions?discussions_q=react-feedback-sidebar) and see what the community is saying.
If you encounter any issues while using this sample, [create a new issue](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Abug-suspected%2Csample%3A%20react-feedback-sidebar&template=bug-report.yml&sample=react-feedback-sidebar&authors=@srpmtt&title=react-feedback-sidebar%20-%20).
For questions regarding this sample, [create a new question](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aquestion%2Csample%3A%20react-feedback-sidebar&template=question.yml&sample=react-feedback-sidebar&authors=@srpmtt&title=react-feedback-sidebar%20-%20).
Finally, if you have an idea for improvement, [make a suggestion](https://github.com/pnp/sp-dev-fx-webparts/issues/new?assignees=&labels=Needs%3A+Triage+%3Amag%3A%2Ctype%3Aenhancement%2Csample%3A%20react-feedback-sidebar&template=question.yml&sample=react-feedback-sidebar&authors=@srpmtt&title=react-feedback-sidebar%20-%20).
## 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.**
<img src="https://pnptelemetry.azurewebsites.net/sp-dev-fx-webparts/samples/react-feedback-sidebar" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

View File

@ -0,0 +1,72 @@
[
{
"name": "pnp-sp-dev-spfx-web-parts-react-feedback-sidebar",
"source": "pnp",
"title": "Feedback Sidebar",
"shortDescription": "This web part displays a sidebar that allows users to provide feedback on various sections of your site.",
"url": "https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-feedback-sidebar",
"downloadUrl": "https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-feedback-sidebar",
"longDescription": [
"This web part displays a sidebar that allows users to provide feedback on various sections of your site."
],
"creationDateTime": "2022-09-14",
"updateDateTime": "2022-09-14",
"products": [
"SharePoint"
],
"metadata": [
{
"key": "CLIENT-SIDE-DEV",
"value": "React"
},
{
"key": "SPFX-VERSION",
"value": "1.15.0"
}
],
"thumbnails": [
{
"type": "image",
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-feedback-sidebar/assets/preview.gif",
"alt": "Web Part Preview"
},
{
"type": "image",
"order": 101,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-feedback-sidebar/assets/preview-img-01.png",
"alt": "Web Part Preview"
},
{
"type": "image",
"order": 102,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-feedback-sidebar/assets/preview-img-02.png",
"alt": "Web Part Preview"
}
],
"authors": [
{
"gitHubAccount": "srpmtt",
"pictureUrl": "https://github.com/srpmtt.png",
"name": "Matteo Serpi"
},
{
"gitHubAccount": "AlessiaDeMartino",
"pictureUrl": "https://github.com/AlessiaDeMartino.png",
"name": "Alessia De Martino"
},
{
"gitHubAccount": "10xMike",
"pictureUrl": "https://github.com/10xMike.png",
"name": "Michele Catena"
}
],
"references": [
{
"name": "Build your first SharePoint client-side web part",
"description": "Client-side web parts are client-side components that run in the context of a SharePoint page. Client-side web parts can be deployed to SharePoint environments that support the SharePoint Framework. You can also use modern JavaScript web frameworks, tools, and libraries to build them.",
"url": "https://learn.microsoft.com/sharepoint/dev/spfx/web-parts/get-started/build-a-hello-world-web-part"
}
]
}
]

View File

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"feedback-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/feedback/FeedbackWebPart.js",
"manifest": "./src/webparts/feedback/FeedbackWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"FeedbackWebPartStrings": "lib/webparts/feedback/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,40 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "Feedback",
"id": "a526cb38-54b1-4704-bb86-295189772026",
"version": "0.0.0.3",
"includeClientSideAssets": true,
"skipFeatureDeployment": true,
"isDomainIsolated": false,
"developer": {
"name": "",
"websiteUrl": "",
"privacyUrl": "",
"termsOfUseUrl": "",
"mpnId": "Undefined-1.15.2"
},
"metadata": {
"shortDescription": {
"default": "Feedback"
},
"longDescription": {
"default": "Feedback"
},
"screenshotPaths": [],
"videoUrl": "",
"categories": []
},
"features": [
{
"title": "feedback-webpart Feature",
"description": "The feature that activates elements of the feedback-webpart solution.",
"id": "a53db913-bbd2-4186-89bf-17a55febf935",
"version": "1.0.0.0"
}
]
},
"paths": {
"zippedPackage": "solution/feedback-webpart.sppkg"
}
}

View File

@ -0,0 +1,6 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://enter-your-SharePoint-site/_layouts/workbench.aspx"
}

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,19 @@
"use strict";
const build = require("@microsoft/sp-build-web");
build.addSuppression(
`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`
);
var getTasks = build.rig.getTasks;
build.rig.getTasks = function () {
var result = getTasks.call(build.rig);
result.set("serve", result.get("serve-deprecated"));
return result;
};
build.tslintCmd.enabled = false;
build.initialize(require("gulp"));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
{
"name": "feedback-webpart",
"version": "0.0.1",
"private": true,
"main": "lib/index.js",
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test",
"bundle": "gulp clean && gulp bundle --debug",
"package": "gulp package-solution --ship",
"ship": "gulp clean && gulp bundle --debug && gulp package-solution --ship"
},
"dependencies": {
"@microsoft/rush-stack-compiler-4.2": "^0.1.2",
"@microsoft/sp-core-library": "1.15.2",
"@microsoft/sp-lodash-subset": "1.15.2",
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
"@microsoft/sp-property-pane": "1.15.2",
"@microsoft/sp-webpart-base": "1.15.2",
"@pnp/logging": "^3.6.0",
"@pnp/sp": "^3.6.0",
"clsx": "^1.2.1",
"office-ui-fabric-react": "7.185.7",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-tss": "^1.0.0",
"tslib": "2.3.1",
"typestyle": "^2.4.0"
},
"devDependencies": {
"@microsoft/eslint-config-spfx": "1.15.2",
"@microsoft/eslint-plugin-spfx": "1.15.2",
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
"@microsoft/sp-build-web": "1.15.2",
"@microsoft/sp-module-interfaces": "1.15.2",
"@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",
"eslint-plugin-react-hooks": "4.3.0",
"gulp": "4.0.2",
"typescript": "4.5.5"
}
}

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,16 @@
import { WebPartContext } from "@microsoft/sp-webpart-base";
import { spfi, SPFI, SPFx } from "@pnp/sp";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/batching";
let _sp: SPFI = null;
export const getSP = (context?: WebPartContext): SPFI => {
if (_sp === null && context !== null) {
_sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};

View File

@ -0,0 +1,29 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "51b3b0b0-3e2d-45ca-98c7-0c95470d4b64",
"alias": "FeedbackWebPart",
"componentType": "WebPart",
"version": "*",
"manifestVersion": 2,
"requiresCustomScript": false,
"supportedHosts": [
"SharePointWebPart",
"TeamsPersonalApp",
"TeamsTab",
"SharePointFullPage"
],
"supportsThemeVariants": true,
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70",
"group": { "default": "Advanced" },
"title": { "default": "Feedback" },
"description": { "default": "Feedback" },
"officeFabricIconFontName": "Feedback",
"properties": {
"description": "Feedback"
}
}
]
}

View File

@ -0,0 +1,63 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
IPropertyPaneConfiguration,
PropertyPaneTextField,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import * as strings from "FeedbackWebPartStrings";
import Feedback from "./components/feedback/Feedback";
import { getSP } from "../../pnpjsConfig";
export interface IFeedbackWebPartProps {
description: string;
}
export default class FeedbackWebPart extends BaseClientSideWebPart<IFeedbackWebPartProps> {
public static user: string;
public static pageUrl: string;
public render(): void {
const element: React.ReactElement = React.createElement(Feedback);
ReactDom.render(element, this.domElement);
}
protected async onInit(): Promise<void> {
super.onInit();
FeedbackWebPart.user = this.context.pageContext.user.loginName;
FeedbackWebPart.pageUrl =
this.context.pageContext.legacyPageContext["serverRequestPath"];
getSP(this.context);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription,
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField("description", {
label: strings.DescriptionFieldLabel,
}),
],
},
],
},
],
};
}
}

View File

@ -0,0 +1,239 @@
import { createUseStyles } from "react-tss/lib";
import { style } from "typestyle";
export enum EAccordionStyle {
primary = "primary",
secondary = "secondary",
default = "default",
}
export const AccordionContainer = style({
flexDirection: "column",
display: "flex",
margin: " 1px 56px",
width: "100%",
});
export const ToggleButton = style({
backgroundColor: "#EDF4F7",
color: "#005980",
flexDirection: "row",
alignItems: "center",
padding: "18px 16px",
display: "flex",
cursor: "pointer",
border: "none",
width: "100%",
});
export const ToggleTitle = style({
fontWeight: "bold",
margin: "-2px 12px 0",
flexGrow: 1,
textAlign: "left",
width: "100%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
export const ToggleIcon = style({
transition: "transform 333ms",
$nest: {
"&.expanded": {
transform: "rotate(90deg) scaleX(-1)",
},
"&.collapsed": {
transform: "rotate(90deg) scaleX(1)",
},
},
});
export const SectionTitle = style({
color: "#454D56",
lineHeight: "36px",
margin: "56px 0 24px 56px",
});
export const ContentSection = style({
$nest: {
"& >*": {
margin: "8px 0",
},
"&.expanded": {
opacity: 1,
},
"&.collapsed": {
paddingBottom: 0,
paddingTop: 0,
opacity: 0,
height: 0,
},
},
backgroundColor: "white",
flexDirection: "column",
transition: "all 333ms",
//padding: "32px 64px",
display: "flex",
border: `1px solid #EDF4F7`,
});
export const AccordionTitle = style({
color: "black",
lineHeight: "27px",
});
export const Subtitle = style({
color: "black",
lineHeight: "21px",
$nest: {
"&.simpleClass": {
paddingLeft: "56px",
},
},
});
export const Abstract = style({
color: "#454D56",
lineHeight: "24px",
});
export const AccordionStyles = createUseStyles({
classes: {
Accordion: {
flexDirection: "column",
display: "flex",
//margin: " 1px 56px",
},
ToggleButton: {
alignItems: "center",
padding: "18px 16px",
display: "flex",
cursor: "pointer",
border: "none",
boxSizing: "border-box",
flex: "1",
[`.${EAccordionStyle.default} &`]: {
backgroundColor: "#EDF4F7",
color: "#005980",
},
[`.${EAccordionStyle.primary} > &`]: {
backgroundColor: "#007DB3",
color: "white",
borderBottom: `1px solid white`,
borderTop: `1px solid white`,
},
[`.${EAccordionStyle.primary}.open > &`]: {
backgroundColor: "#00354C",
color: "white",
borderBottom: `1px solid white`,
borderTop: `1px solid white`,
},
[`.${EAccordionStyle.secondary} > &`]: {
backgroundColor: "white",
color: "#005980",
borderBottom: `1px solid #EDF4F7`,
borderTop: `1px solid #EDF4F7`,
},
[`.${EAccordionStyle.secondary}.open > &`]: {
backgroundColor: "#EDF4F7",
color: "#005980",
borderBottom: `1px solid #EDF4F7`,
borderTop: `1px solid #EDF4F7`,
},
},
ToggleTitle: {
boxSizing: "border-box",
textAlign: "left",
width: "100%",
"text-overflow": "ellipsis",
overflow: "hidden",
"white-space": "nowrap",
lineHeight: "24px",
},
AccordionIcon: {
height: "24px",
width: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
ToggleIcon: {
height: "24px",
width: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginLeft: "auto",
"&.expanded": {
transform: "rotate(90deg) scaleX(-1)",
},
"&.collapsed": {
transform: "rotate(90deg) scaleX(1)",
},
transition: "transform 333ms",
},
SectionTitle: {
color: "#454D56",
lineHeight: "36px",
margin: "56px 0 24px 56px",
},
Tooltip: {
flex: "1",
padding: "0 12px",
boxSizing: "border-box",
maxWidth: "calc(100% - 24px - 9px)",
},
},
});
export const AccordionContentStyles = createUseStyles({
classes: {
ContentSection: {
"& >*": {
margin: "0px 0",
},
"&.expanded": {
opacity: 1,
},
"&.collapsed": {
paddingBottom: 0,
paddingTop: 0,
opacity: 0,
height: 0,
},
backgroundColor: "white",
flexDirection: "column",
transition: "all 333ms",
//padding: "32px 64px",
display: "flex",
border: `1px solid "#EDF4F7"`,
},
Title: {
"&.simpleClass": {
paddingLeft: "56px",
},
color: "black",
lineHeight: "27px",
},
Subtitle: {
color: "black",
lineHeight: "21px",
},
Abstract: {
color: "#454D56",
lineHeight: "24px",
},
},
});

View File

@ -0,0 +1,71 @@
import clsx from "clsx";
import * as React from "react";
import { useState } from "react";
import { AccordionProps } from "../../utils/Types";
import { Image, ImageEnum } from "../image/Image";
import { AccordionStyles, AccordionContentStyles } from "./Accordion.styles";
export const Accordion = ({
headText,
expanded,
children,
className,
style = "default",
}: AccordionProps): JSX.Element => {
const accordionStyle = AccordionStyles();
const accordionContentStyle = AccordionContentStyles();
const [isOpen, setOpen] = useState(expanded);
const PanelToggleHandler = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
): void => {
e.stopPropagation();
setOpen((st) => !st);
};
const wChilds = Array.isArray(children) ? children : [children];
return (
<div
className={clsx(
accordionStyle.Accordion,
className,
style,
isOpen && "open"
)}
>
<button
className={accordionStyle.ToggleButton}
onClick={PanelToggleHandler}
>
<div className={accordionStyle.ToggleTitle}>{headText}</div>
<Image
imageId={ImageEnum.angle}
className={clsx(
accordionStyle.ToggleIcon,
isOpen ? "expanded" : "collapsed"
)}
/>
</button>
<div
className={clsx(
accordionContentStyle.ContentSection,
"accordion-body",
isOpen ? "expanded" : "collapsed"
)}
>
{wChilds
.filter(() => isOpen)
.map((child, idx) => {
if (child) {
const key = child.key || `content-${idx}`;
return { ...child, key };
}
return <React.Fragment key={`tmp-${idx}`} />;
})}
</div>
</div>
);
};

View File

@ -0,0 +1,59 @@
import { createUseStyles } from "react-tss/lib";
import { EButtonStyle } from "../../utils/Types";
export const ButtonStyles = createUseStyles({
classes: {
button: {
borderRadius: "4px",
cursor: "pointer",
maxWidth: "100%",
padding: "16px 42px",
letterSpacing: "0",
lineHeight: "16px",
textAlign: "center",
whiteSpace: "normal",
wordBreak: "break-word",
[`&.${EButtonStyle.primary}`]: {
color: "white",
backgroundColor: "#007DB3",
},
[`&.${EButtonStyle.secondary}`]: {
color: "#007DB3",
backgroundColor: "white",
border: `2px solid #007DB3`,
},
[`&.${EButtonStyle.tertiary}`]: {
color: "#007DB3",
backgroundColor: "white",
lineHeight: "21px",
padding: "0",
textTransform: "capitalize",
},
[`&.${EButtonStyle.disabled}`]: {
color: "white",
backgroundColor: "#D3D3D3",
cursor: "not-allowed",
},
},
buttonIcon: {
marginRight: "16px",
height: "20px",
verticalAlign: "middle",
width: "20px",
[`.${EButtonStyle.primary} &`]: {
fill: "white",
},
[`.${EButtonStyle.secondary} &`]: {
fill: "#007DB3",
},
[`.${EButtonStyle.tertiary} &`]: {
fill: "#007DB3",
},
},
buttonContent: {
verticalAlign: "middle",
},
},
});

View File

@ -0,0 +1,29 @@
import * as React from "react";
import clsx from "clsx";
import { ButtonProps } from "../../utils/Types";
import { ButtonStyles } from "./Button.styles";
export const Button = ({
children,
type = "button",
onClick,
styleType,
className,
position = "left",
disabled = false,
}: ButtonProps): JSX.Element => {
const style = ButtonStyles();
return (
<button
className={clsx(style.button, styleType, className || "")}
type={type}
onClick={onClick}
disabled={disabled}
>
{position === "left"}
<span className={style.buttonContent}>{children}</span>
{position === "right"}
</button>
);
};

View File

@ -0,0 +1,32 @@
import * as React from "react";
import { useState } from "react";
import { FeedbackProps } from "../../utils/Types";
import { FeedbackButton } from "../feedbackButton/FeedbackButton";
import { FeedbackWrapper } from "../feedbackWrapper/FeedbackWrapper";
const Feedback = ({ data }: FeedbackProps): JSX.Element => {
const [show, setShow] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false);
const onOpen = React.useCallback<
React.MouseEventHandler<HTMLButtonElement>
>(() => {
setShow(true);
setOpen(true);
}, []);
const onClose = React.useCallback<
React.MouseEventHandler<HTMLButtonElement>
>(() => {
setShow(false);
setOpen(false);
}, []);
return (
<>
<FeedbackButton onClick={onOpen} />
{open && <FeedbackWrapper open={show} data={data} onClose={onClose} />}
</>
);
};
export default Feedback;

View File

@ -0,0 +1,23 @@
import { createUseStyles } from "react-tss/lib";
export const FeedbackButtonStyles = createUseStyles({
classes: {
feedbackButton: {
borderRadius: "4px 0 0 4px",
borderRight: 0,
paddingRight: "21px",
position: "fixed",
right: "-4px",
top: "calc(100% - 130px)",
transform: "translateY(-50%)",
zIndex: 9,
"& path": {
fill: "#007DB3",
},
"& svg": {
marginLeft: "16px",
marginRight: "0px",
},
},
},
});

View File

@ -0,0 +1,22 @@
import * as React from "react";
import * as strings from "FeedbackWebPartStrings";
import { FeedbackButtonProps } from "../../utils/Types";
import { FeedbackButtonStyles } from "./FeedbackButton.styles";
import { Button } from "../button/Button";
export const FeedbackButton = ({
onClick,
}: FeedbackButtonProps): JSX.Element => {
const style = FeedbackButtonStyles();
return (
<Button
className={style.feedbackButton}
position="right"
styleType="secondary"
onClick={onClick}
>
{strings.ButtonLabel}
</Button>
);
};

View File

@ -0,0 +1,62 @@
import { createUseStyles } from "react-tss/lib";
import { ETemplateSectionFeedback } from "../../utils/Types";
export const FeedbackFormStyles = createUseStyles({
classes: {
feedbackSection: {
width: "90%",
[`&.${ETemplateSectionFeedback.main}`]: {},
[`&.${ETemplateSectionFeedback.section}`]: {
padding: "16px 10px",
},
},
feedbackSectionRow: {
position: "relative",
"&.feedbackButton": {
display: "flex",
justifyContent: "center",
marginTop: "32px",
},
"&.feedbackSent": {
display: "flex",
justifyContent: "center",
},
"&.feedbackLoader": {
width: "100%",
paddingTop: "50%",
},
},
feedbackSectionTitle: {
lineHeight: "30px",
[`.${ETemplateSectionFeedback.main} &`]: {},
[`.${ETemplateSectionFeedback.section} &`]: {},
},
feedbackSectionTextarea: {
minHeight: "96px",
width: "100%",
border: "1px solid #454D56",
borderRadius: "4px",
marginTop: "8px",
paddingTop: "16px",
paddingLeft: "16px",
resize: "none",
[`.${ETemplateSectionFeedback.main} &`]: {},
[`.${ETemplateSectionFeedback.section} &`]: {},
},
feedbackSectionRate: {
marginTop: "16px",
},
feedbackSectionButton: {
borderRadius: "4px",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
width: "185px",
height: "48px",
},
feedbackTitle: {
lineHeight: "27px",
},
},
});

View File

@ -0,0 +1,168 @@
import * as React from "react";
import { useState, useCallback, useMemo } from "react";
import clsx from "clsx";
import { EState, FeedbackFormProps } from "../../utils/Types";
import { FeedbackFormStyles } from "./FeedbackForm.style";
import FeedbackWebPart from "../../FeedbackWebPart";
import { Button } from "../button/Button";
import * as strings from "FeedbackWebPartStrings";
import { Loader } from "../loader/Loader";
import { Image, ImageEnum } from "../image/Image";
import { sendFeedback } from "../../utils/ApiHelper";
import { Rate } from "../rate/Rate";
import { TextArea } from "../textArea/TextArea";
export const FeedbackForm = ({
sectionTitle: componentName,
title,
template = "section",
}: FeedbackFormProps): JSX.Element => {
const style = FeedbackFormStyles();
const [value, setValue] = useState<string>("");
const [rate, setRate] = useState<string>("0");
const [state, setState] = useState<keyof typeof EState>("initial");
const onClick = useCallback<
React.MouseEventHandler<HTMLButtonElement>
>(() => {
setState("initial");
setValue("");
setRate("0");
}, []);
const onSubmit = useCallback(
async (event) => {
setState("loading");
sendFeedback({
section: componentName,
rating: Number(rate),
comment: value as string,
upn: FeedbackWebPart.user,
})
.then((result) => {
setState("success");
})
.catch((error) => {
console.error(error);
setState("failure");
});
},
[componentName, value, rate, name, title]
);
const onChange = useCallback<React.ChangeEventHandler<HTMLTextAreaElement>>(
(event) => {
setValue(event.target.value);
},
[]
);
const onRate = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(event) => {
setRate(event.target.value);
},
[]
);
const Component = useMemo<JSX.Element | null>(() => {
switch (state) {
case "initial": {
return (
<>
<div className={style.feedbackSectionRow}>
<p className={style.feedbackSectionTitle}>{title}</p>
</div>
<div className={style.feedbackSectionRow}>
<TextArea
className={style.feedbackSectionTextarea}
onChange={onChange}
value=""
placeholder={strings.PlaceholderTextarea}
/>
</div>
<div className={style.feedbackSectionRow}>
<Rate
className={style.feedbackSectionRate}
onRate={onRate}
rateLenght={5}
rate={0}
name={componentName}
/>
</div>
<div className={clsx(style.feedbackSectionRow, "feedbackButton")}>
<Button
className={style.feedbackSectionButton}
styleType={
!(value.length || rate !== "0") ? "disabled" : "secondary"
}
onClick={onSubmit}
disabled={!(value.length || rate !== "0")}
>
{strings.Send}
</Button>
</div>
</>
);
}
case "loading": {
return (
<>
<div className={clsx(style.feedbackSectionRow, "feedbackLoader")}>
<Loader />
</div>
</>
);
}
case "success": {
return (
<>
<div className={clsx(style.feedbackSectionRow, "feedbackSent")}>
<Image imageId={ImageEnum.success} />
</div>
<div className={clsx(style.feedbackSectionRow, "feedbackSent")}>
<p className={style.feedbackTitle}>{strings.FeedbackSent}</p>
</div>
<div className={clsx(style.feedbackSectionRow, "feedbackButton")}>
<Button
className={style.feedbackSectionButton}
styleType="secondary"
onClick={onClick}
>
{strings.NewFeedback}
</Button>
</div>
</>
);
}
case "failure": {
return (
<>
<div className={clsx(style.feedbackSectionRow, "feedbackSent")}>
<Image imageId={ImageEnum.error} />
</div>
<div className={clsx(style.feedbackSectionRow, "feedbackSent")}>
<p className={style.feedbackTitle}>{strings.Error}</p>
</div>
<div className={clsx(style.feedbackSectionRow, "feedbackButton")}>
<Button
className={style.feedbackSectionButton}
styleType="secondary"
onClick={onClick}
>
{strings.TryAgain}
</Button>
</div>
</>
);
}
default: {
return null;
}
}
}, [state, onChange, onRate, onClick, name, title, onSubmit]);
return (
<div className={clsx(style.feedbackSection, template)}>{Component}</div>
);
};

View File

@ -0,0 +1,120 @@
import { createUseStyles } from "react-tss/lib";
export const FeedbackWrapperStyles = createUseStyles({
classes: {
FeedbackSection: {
background: "rgba(0, 0, 0, 0)",
animation: `$fadeIn 0.8s ease 0s 1 forwards`,
width: "100%",
position: "fixed",
top: 0,
left: 0,
height: "100vh",
zIndex: 9,
},
FeedbackWrapper: {
backgroundColor: "white",
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "center",
position: "fixed",
top: 0,
right: 0,
height: "100vh",
zIndex: 9,
animation: `$scrollIn 0.8s ease 0s 1 forwards`,
maxWidth: "340px",
overflowY: "auto",
},
FeedbackWrapperRow: {
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
alignItems: "center",
//margin: "0 32px",
width: "calc(100% - 32px * 2)",
"&.to-end": {
justifyContent: "flex-end",
paddingRight: "20px",
paddingBottom: "25px",
paddingTop: "100px",
marginLeft: "auto",
width: "initial",
},
"&.no-margin": {
margin: 0,
width: "100%",
},
"&.accordion-row": {
//backgroundColor: "#007DB3",
backgroundColor: "white",
flex: 1,
marginTop: "72px",
},
"&.loader-row": {
flex: 1,
position: "relative",
},
},
FeedbackWrapperClose: {
background: "transparent",
cursor: "pointer",
border: "none",
"& svg": {
width: "100%",
height: "100%",
fill: "#007DB3",
},
},
FeedbackWrapperTitle: {
fontWeight: "bold",
color: "black",
lineHeight: "36px",
marginBottom: "8px",
width: "100%",
},
FeedbackWrapperDescription: {
color: "#454D56",
lineHeight: "24px",
marginBottom: "32px",
width: "100%",
},
FeedbackWrapperList: {
listStyle: "none",
width: "100%",
paddingLeft: "0px",
margin: "0px",
},
FeedbackWrapperItem: {
width: "100%",
},
FeedbackSectionAccordion: {
padding: "0",
"& > button": {
borderTop: "none !important",
},
"& button > div": {
width: "100%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
},
"& button > svg": {
marginRight: "10px",
height: "20px",
width: "20px",
},
},
},
keyframes: {
fadeIn: {
from: { background: "rgba(0, 0, 0, 0)" },
to: { background: "rgba(0, 0, 0, 0.5)" },
},
scrollIn: {
from: { transform: "translateX(100%)" },
to: { transform: "translateX(0%)" },
},
},
});

View File

@ -0,0 +1,126 @@
import * as React from "react";
import { useRef, useMemo, useCallback, useState, useEffect } from "react";
import clsx from "clsx";
import * as strings from "FeedbackWebPartStrings";
import { buildFeedbackConfigFromPage } from "../../utils/SpHelper";
import { FeedbackConfig, FeedbackWrapperProps } from "../../utils/Types";
import { FeedbackWrapperStyles } from "./FeedbackWrapper.styles";
import { Accordion } from "../accordion/Accordion";
import { Image, ImageEnum } from "../image/Image";
import { Loader } from "../loader/Loader";
import { FeedbackForm } from "../feedbackForm/FeedbackForm";
export const FeedbackWrapper = ({
onClose,
open,
}: FeedbackWrapperProps): JSX.Element => {
const style = FeedbackWrapperStyles();
const [sections, setSections] = useState<FeedbackConfig[]>([]);
useEffect(() => {
buildFeedbackConfigFromPage()
.then((sections) => {
setSections(sections);
})
.catch((error) => {
console.error(error);
});
}, []);
const wrapperRef = useRef<HTMLDivElement>(null);
const SectionsList = useMemo<JSX.Element[]>(
() =>
sections.map((section) => (
<li
className={style.FeedbackWrapperItem}
key={`${section.title.replace(/ /, "-")}`}
>
<ul className={style.FeedbackWrapperList}>
<Accordion
className={style.FeedbackSectionAccordion}
headText={section.title}
style="secondary"
>
<FeedbackForm
sectionTitle={section.title}
title={strings.SectionTitle}
template="section"
/>
</Accordion>
</ul>
</li>
)),
[sections]
);
const onOuterClick = useCallback(
(event) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
onClose(event);
}
},
[onClose, wrapperRef]
);
return (
<div className={clsx(style.FeedbackSection)} onClick={onOuterClick}>
<div
ref={wrapperRef}
className={clsx(
style.FeedbackWrapper,
open ? "animate-in" : "animate-out"
)}
>
<div className={clsx(style.FeedbackWrapperRow, "to-end")}>
<button
className={style.FeedbackWrapperClose}
type="button"
onClick={onClose}
>
<Image imageId={ImageEnum.close} />
</button>
</div>
<div
className={clsx(
style.FeedbackWrapperRow,
SectionsList.length === 0 && "loader-row"
)}
>
<div className={clsx(style.FeedbackWrapperRow, "no-margin")}>
<p className={style.FeedbackWrapperTitle}>
{strings.FeedbackWrapperTitle}
</p>
<p className={style.FeedbackWrapperDescription}>
{strings.FeedbackWrapperDescription}
</p>
</div>
{SectionsList.length > 0 ? (
<div className={clsx(style.FeedbackWrapperRow, "no-margin")}>
<FeedbackForm
sectionTitle="generic"
title={strings.SectionTitle}
template="main"
/>
</div>
) : (
<div className={clsx(style.FeedbackWrapperRow, "loader-row")}>
<Loader />
</div>
)}
</div>
{SectionsList.length > 0 && (
<div
className={clsx(
style.FeedbackWrapperRow,
"no-margin",
"accordion-row"
)}
>
<ul className={style.FeedbackWrapperList}>{SectionsList}</ul>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,14 @@
import * as React from "react";
export const Angle = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
focusable="false"
{...props}
>
<path d="M16.5141 12.7968L10.1391 19.1718C9.69844 19.6124 8.98594 19.6124 8.55 19.1718L7.49063 18.1124C7.05 17.6718 7.05 16.9593 7.49063 16.5233L12.0094 12.0046L7.49063 7.48584C7.05 7.04521 7.05 6.33271 7.49063 5.89678L8.54531 4.82803C8.98594 4.3874 9.69844 4.3874 10.1344 4.82803L16.5094 11.203C16.9547 11.6437 16.9547 12.3562 16.5141 12.7968Z" />
</svg>
);

View File

@ -0,0 +1,15 @@
import * as React from "react";
export const Close = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
focusable="false"
{...props}
>
<path d="M9.52695 6.99967L13.3175 3.20914C13.7826 2.74399 13.7826 1.98983 13.3175 1.5243L12.4751 0.681871C12.0099 0.21672 11.2557 0.21672 10.7902 0.681871L6.99967 4.4724L3.20914 0.681871C2.74399 0.21672 1.98983 0.21672 1.5243 0.681871L0.681871 1.5243C0.21672 1.98945 0.21672 2.74361 0.681871 3.20914L4.4724 6.99967L0.681871 10.7902C0.21672 11.2554 0.21672 12.0095 0.681871 12.4751L1.5243 13.3175C1.98945 13.7826 2.74399 13.7826 3.20914 13.3175L6.99967 9.52695L10.7902 13.3175C11.2554 13.7826 12.0099 13.7826 12.4751 13.3175L13.3175 12.4751C13.7826 12.0099 13.7826 11.2557 13.3175 10.7902L9.52695 6.99967Z" />
</svg>
);

View File

@ -0,0 +1,15 @@
import * as React from "react";
export const Error = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
fill="#AA224F"
focusable="false"
{...props}
>
<path d="M13.7909 10L19.4767 4.3142C20.1744 3.61648 20.1744 2.48523 19.4767 1.78693L18.2131 0.523295C17.5153 -0.174432 16.3841 -0.174432 15.6858 0.523295L10 6.20909L4.3142 0.523295C3.61648 -0.174432 2.48523 -0.174432 1.78693 0.523295L0.523295 1.78693C-0.174432 2.48466 -0.174432 3.61591 0.523295 4.3142L6.20909 10L0.523295 15.6858C-0.174432 16.3835 -0.174432 17.5148 0.523295 18.2131L1.78693 19.4767C2.48466 20.1744 3.61648 20.1744 4.3142 19.4767L10 13.7909L15.6858 19.4767C16.3835 20.1744 17.5153 20.1744 18.2131 19.4767L19.4767 18.2131C20.1744 17.5153 20.1744 16.3841 19.4767 15.6858L13.7909 10Z" />
</svg>
);

View File

@ -0,0 +1,73 @@
import * as React from "react";
import { Angle } from "./Angle";
import { Close } from "./Close";
import { Spinner } from "./Spinner";
import { Success } from "./Success";
import { Error } from "./Error";
import { StarLeft } from "./StarLeft";
import { StarRight } from "./StarRight";
//* eslint-disable @typescript-eslint/camelcase */
export enum ImageEnum {
angle = "angle",
close = "close",
spinner = "spinner",
success = "success",
error = "error",
starLeft = "star-left",
starRight = "star-right",
}
//* eslint-enable @typescript-eslint/camelcase */
export interface ImageProps
extends React.SVGProps<SVGSVGElement | HTMLImageElement> {
imageId: ImageEnum;
}
export const Image = (props: ImageProps): JSX.Element => {
const { imageId, ...oth } = props;
let result = <React.Fragment />;
switch (imageId) {
case ImageEnum.angle: {
result = <Angle {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
case ImageEnum.close: {
result = <Close {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
case ImageEnum.spinner: {
result = <Spinner {...(oth as React.SVGProps<HTMLImageElement>)} />;
break;
}
case ImageEnum.success: {
result = <Success {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
case ImageEnum.error: {
result = <Error {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
case ImageEnum.starLeft: {
result = <StarLeft {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
case ImageEnum.starRight: {
result = <StarRight {...(oth as React.SVGProps<SVGSVGElement>)} />;
break;
}
}
return result;
};
export default {
Image,
ImageEnum,
Angle,
Close,
Spinner,
Success,
Error,
StarLeft,
StarRight,
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,15 @@
import * as React from "react";
export const StarLeft = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
width="11"
height="20"
viewBox="0 0 11 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
focusable="false"
{...props}
>
<path d="M10.4128 0.833008C10.0045 0.833008 9.59627 1.0443 9.38498 1.47046L7.04644 6.212L1.81428 6.97122C0.875999 7.1073 0.499971 8.26404 1.1804 8.92656L4.96575 12.6152L4.07045 17.8259C3.90929 18.7606 4.89413 19.4733 5.73213 19.0328L10.4128 16.576V0.833008Z" />
</svg>
);

View File

@ -0,0 +1,17 @@
import * as React from "react";
export const StarRight = (
props: React.SVGProps<SVGSVGElement>
): JSX.Element => (
<svg
width="10"
height="20"
viewBox="0 0 10 20"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
focusable="false"
{...props}
>
<path d="M0.421197 0.833008C0.829455 0.833008 1.23772 1.0443 1.44901 1.47046L3.78754 6.212L9.01971 6.97122C9.95798 7.1073 10.334 8.26404 9.65358 8.92656L5.86823 12.6152L6.76354 17.8259C6.92469 18.7606 5.93986 19.4733 5.10185 19.0328L0.421197 16.576V0.833008Z" />
</svg>
);

View File

@ -0,0 +1,15 @@
import * as React from "react";
export const Success = (props: React.SVGProps<SVGSVGElement>): JSX.Element => (
<svg
width="24"
height="18"
viewBox="0 0 24 18"
xmlns="http://www.w3.org/2000/svg"
fill="#049894"
focusable="false"
{...props}
>
<path d="M8.15146 17.6465L0.351457 9.80163C-0.117152 9.33032 -0.117152 8.56616 0.351457 8.0948L2.04847 6.38798C2.51708 5.91663 3.27693 5.91663 3.74554 6.38798L8.99999 11.6726L20.2544 0.353478C20.7231 -0.117826 21.4829 -0.117826 21.9515 0.353478L23.6485 2.0603C24.1171 2.5316 24.1171 3.29577 23.6485 3.76712L9.84852 17.6465C9.37986 18.1178 8.62007 18.1178 8.15146 17.6465Z" />
</svg>
);

View File

@ -0,0 +1,34 @@
import { createUseStyles } from "react-tss/lib";
export const LoaderStyles = createUseStyles({
keyframes: {
rotation: {
from: {
transform: "rotate(0deg)",
},
to: {
transform: "rotate(359deg)",
},
},
},
classes: {
overlay: {
backgroundcolor: "white",
top: 0,
left: 0,
width: "100%",
zIndex: 8,
cursor: "default",
height: "calc(100% + 1px)",
position: "absolute",
borderRadius: 16,
},
rotate: {
top: "calc(50% - 40px)",
left: "calc(50% - 40px)",
zIndex: 9,
position: "relative",
animation: `$rotation 1.2s linear infinite`,
},
},
});

View File

@ -0,0 +1,12 @@
import * as React from "react";
import { LoaderStyles } from "./Loader.styles";
import { Image, ImageEnum } from "../image/Image";
export const Loader = (): JSX.Element => {
const style = LoaderStyles();
return (
<div className={style.overlay}>
<Image imageId={ImageEnum.spinner} className={style.rotate} />
</div>
);
};

View File

@ -0,0 +1,44 @@
import { createUseStyles } from "react-tss/lib";
export const RateStyles = createUseStyles({
classes: {
rate: {
border: "0",
boxSizing: "border-box",
margin: "0",
outline: "none",
padding: "0",
},
rateList: {
listStyle: "none",
alignItems: "center",
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
},
rateItem: {
height: "18px",
width: "9px",
"&.margin": {
marginRight: "9px",
},
},
rateInput: {
display: "none",
},
rateLabel: {
cursor: "pointer",
height: "100%",
width: "100%",
},
rateIcon: {
fill: "#EDF4F7",
//height: '100%',
//width: '100%',
"&.checked": {
fill: "#007DB3",
},
},
},
});

View File

@ -0,0 +1,76 @@
import * as React from "react";
import { useCallback } from "react";
import { useEffect, useMemo, useState } from "react";
import { RateStyles } from "./Rate.styles";
import { Image, ImageEnum } from "../image/Image";
import clsx from "clsx";
import { RateProps } from "../../utils/Types";
const handleRate = (value: number): number => {
if (value < 0) {
return 0;
} else if (value > 5) {
return 5;
}
return value !== Math.floor(value) ? Math.floor(value) + 0.5 : value;
};
export const Rate = ({
onRate: onRateProp,
rate,
name,
className,
rateLenght,
}: RateProps): JSX.Element => {
const style = RateStyles();
const [currentRate, setCurrentRate] = useState<number>(handleRate(rate));
useEffect(() => {
setCurrentRate(handleRate(rate));
}, [rate]);
const onRate = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(event) => {
onRateProp(event);
setCurrentRate(parseFloat(event.target.value));
},
[onRateProp]
);
const List = useMemo<JSX.Element[]>(
() =>
Array.from({ length: rateLenght * 2 }).map((_, index) => (
<li
key={`${index}`}
className={clsx(style.rateItem, index % 2 !== 0 && "margin")}
>
<input
className={style.rateInput}
type="radio"
onChange={onRate}
name={name}
id={`${name}-${index}`}
value={(index + 1) / 2}
/>
<label className={style.rateLabel} htmlFor={`${name}-${index}`}>
<Image
className={clsx(
style.rateIcon,
currentRate >= (index + 1) / 2 && "checked"
)}
imageId={ImageEnum[index % 2 === 0 ? "starLeft" : "starRight"]}
/>
</label>
</li>
)),
[onRate, currentRate, name, currentRate]
);
return (
<div className={clsx(style.rate, className)}>
<ul className={style.rateList}>{List}</ul>
</div>
);
};

View File

@ -0,0 +1,14 @@
import { createUseStyles } from "react-tss/lib";
export const TextAreaStyle = createUseStyles({
classes: {
textarea: {
color: "#454D56",
lineHeight: "16px",
border: `1px solid #454D56`,
borderRadius: "4px",
//padding: "16px",
width: "90%",
},
},
});

View File

@ -0,0 +1,36 @@
import * as React from "react";
import { useState, useEffect, useCallback } from "react";
import { TextAreaStyle } from "./TextArea.styles";
import clsx from "clsx";
import { TextareaProps } from "../../utils/Types";
export const TextArea = ({
className,
onChange: onChangeProp,
value: valueProp,
placeholder,
}: TextareaProps): JSX.Element => {
const style = TextAreaStyle();
const [value, setValue] = useState(valueProp);
useEffect(() => {
setValue(valueProp);
}, [valueProp]);
const onChange = useCallback<React.ChangeEventHandler<HTMLTextAreaElement>>(
(event) => {
onChangeProp(event);
setValue(event.target.value);
},
[onChangeProp]
);
return (
<textarea
className={clsx(style.textarea, className)}
onChange={onChange}
value={value}
placeholder={placeholder}
></textarea>
);
};

View File

@ -0,0 +1,18 @@
define([], function () {
return {
PropertyPaneDescription: "Description",
BasicGroupName: "Group Name",
DescriptionFieldLabel: "Description Field",
ButtonLabel: "Feedback",
SectionTitle: "What do you think about this section?",
FeedbackWrapperDescription:
"Your feedbacks are useful to keep improving the site experience!",
FeedbackWrapperTitle: "Give a feedback about this site!",
PlaceholderTextarea: "Leave your comment here",
FeedbackSent: "Feedback sent!",
NewFeedback: "New feedback",
Error: "An error has occurred",
TryAgain: "Try Again",
Send: "Send feedback",
};
});

View File

@ -0,0 +1,20 @@
declare interface IFeedbackWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
ButtonLabel: string;
SectionTitle: string;
FeedbackWrapperDescription: string;
FeedbackWrapperTitle: string;
PlaceholderTextarea: string;
FeedbackSent: string;
NewFeedback: string;
Error: string;
TryAgain: string;
Send: string;
}
declare module "FeedbackWebPartStrings" {
const strings: IFeedbackWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,7 @@
import { FeedbackPayload } from "./Types";
export const sendFeedback = async (body: FeedbackPayload): Promise<boolean> => {
// TODO: send payload to backend
console.log(body);
return true;
};

View File

@ -0,0 +1 @@
export const WEBPART_NAME = "Feedback";

View File

@ -0,0 +1,41 @@
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
import "@pnp/sp/clientside-pages/web";
import { IClientsidePage } from "@pnp/sp/clientside-pages";
import * as Constants from "./Constants";
import { FeedbackConfig } from "./Types";
import { getSP } from "../../../pnpjsConfig";
import FeedbackWebPart from "../FeedbackWebPart";
export const buildFeedbackConfigFromPage = async () => {
let config: FeedbackConfig[] = [];
let sp = getSP();
const page: IClientsidePage = await sp.web.loadClientsidePage(
FeedbackWebPart.pageUrl
);
page.findControl((c: any) => {
config.push({
title: c.title,
position: c.order,
});
return false;
});
// remove duplicates
config = config.filter(
(value, index, self) =>
index ===
self.findIndex(
(t) =>
t.title &&
t.title === value.title &&
value.title !== Constants.WEBPART_NAME
)
);
return config.sort((s) => s.position); //sort by webpart position
};

View File

@ -0,0 +1,111 @@
import { EAccordionStyle } from "../components/accordion/Accordion.styles";
export type FeedbackProps = {
data: FeedbackWrapperProps["data"];
};
export type FeedbackWrapperProps = {
data: FeedbackConfig[];
onClose: React.MouseEventHandler<HTMLButtonElement>;
open: boolean;
};
export type FeedbackConfig = {
title: string;
position: number;
};
export type SpArea = {
title: string;
enabled: boolean;
position: string;
id: number;
};
export type FeedbackSection = {
title: string;
enabled: boolean;
position: string;
databaseKey: string;
};
export type SpSection = {
title: string;
area: number;
enabled: boolean;
position: string;
databaseKey: string;
};
export type FeedbackButtonProps = {
onClick: React.MouseEventHandler<HTMLButtonElement>;
};
export type ButtonProps = {
children: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
type?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
styleType: keyof typeof EButtonStyle;
className?: string;
position?: keyof typeof EPositionIcon;
disabled?: boolean;
};
export enum EButtonStyle {
primary = "primary",
secondary = "secondary",
tertiary = "tertiary",
disabled = "disabled",
}
export enum EPositionIcon {
left = "left",
right = "right",
}
export enum ETemplateSectionFeedback {
main = "main",
section = "section",
}
export type FeedbackFormProps = {
sectionTitle: string;
title: string;
template?: keyof typeof ETemplateSectionFeedback;
};
export enum EState {
initial = "initial",
loading = "loading",
success = "success",
failure = "failure",
}
export type FeedbackPayload = {
section: string;
rating: number;
comment: string;
upn: string;
};
export type TextareaProps = {
onChange: React.ChangeEventHandler<HTMLTextAreaElement>;
value: React.TextareaHTMLAttributes<HTMLTextAreaElement>["value"];
placeholder: React.TextareaHTMLAttributes<HTMLTextAreaElement>["placeholder"];
className?: string;
};
export type RateProps = {
onRate: React.ChangeEventHandler<HTMLInputElement>;
rate: number;
rateLenght: number;
name: string;
className?: string;
};
export type AccordionProps = {
headText: string;
expanded?: boolean;
children?: JSX.Element[] | JSX.Element | undefined;
style?: keyof typeof EAccordionStyle;
className?: string;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

View File

@ -0,0 +1,24 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/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,
"noImplicitAny": true,
"typeRoots": ["./node_modules/@types", "./node_modules/@microsoft"],
"types": ["webpack-env"],
"lib": ["es2015", "dom", "es2015.collection", "es2015.promise"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}

View File

@ -1,7 +1,7 @@
// For more information on how to run this SPFx project in a VS Code Remote Container, please visit https://aka.ms/spfx-devcontainer
{
"name": "SPFx 1.13.1",
"image": "docker.io/m365pnp/spfx:1.13.1",
"name": "SPFx 1.15.0",
"image": "docker.io/m365pnp/spfx:1.15.0",
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.