@webority/insights-react 0.0.0-stage → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) Webority Technologies. All rights reserved.
2
+
3
+ This software and its source code are proprietary and confidential to
4
+ Webority Technologies. Use is restricted to Webority products and teams under
5
+ internal license. Redistribution outside Webority Technologies is prohibited.
package/README.md CHANGED
@@ -1,3 +1,105 @@
1
- # Temporary Holding Version
1
+ # @webority/insights-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Product feedback for a React portal: the one NPS question and a feedback button. Both post to the
4
+ product's feedback API, which the product's backend hosts through Webority.Insights. It is built on
5
+ `@webority/ui-react`, so it looks like the rest of the portal with no extra CSS.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @webority/insights-react @webority/ui-react react-bootstrap bootstrap@5.3.3
11
+ ```
12
+
13
+ `@webority/ui-react`, `react`, `react-dom`, `react-bootstrap` and `bootstrap` are peer
14
+ dependencies: the portal already has them. Pin `@webority/insights-react` to the same exact
15
+ version as `@webority/ui-react`.
16
+
17
+ ## Set up the provider
18
+
19
+ Give the provider a client built on the portal's own signed-in HTTP instance. That instance already
20
+ carries the bearer token and refreshes it, so the feedback calls sign in the same way as every
21
+ other call.
22
+
23
+ ```jsx
24
+ import { createInsightsFeedbackClient, InsightsFeedbackProvider } from "@webority/insights-react";
25
+ import { userClient } from "./services/api/clients";
26
+
27
+ const feedbackClient = createInsightsFeedbackClient(userClient);
28
+
29
+ export function App() {
30
+ return (
31
+ <InsightsFeedbackProvider client={feedbackClient}>
32
+ <Routes />
33
+ </InsightsFeedbackProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ `createInsightsFeedbackClient(http, options)` accepts any axios-like instance. `options.prefix`
39
+ replaces the API prefix, which defaults to `/api/insights/feedback`. The client returns
40
+ `{ getNps(), answerNps(body), leaveComment(body) }`. Every method rejects with an
41
+ `InsightsFeedbackApiError` carrying `code`, `status` and a `message` written for the person, which
42
+ the components show as-is.
43
+
44
+ ## Components
45
+
46
+ ### `<NpsPrompt />`
47
+
48
+ The "how likely are you to recommend us" question. It asks the server once per mount whether the
49
+ question is due, and renders nothing while it loads, when it is not due, or when that read fails.
50
+ When it is due it shows a 0 to 10 scale, a comment box once a score is chosen, Send and Not now.
51
+ Send thanks the person. Not now is a permanent answer: the server records it and the question does
52
+ not come back.
53
+
54
+ ```jsx
55
+ <NpsPrompt question="How likely are you to recommend us to a colleague?" />
56
+ ```
57
+
58
+ | Prop | Default |
59
+ |---|---|
60
+ | `question` | "How likely are you to recommend us to a colleague?" |
61
+ | `lowLabel` | "Not likely" |
62
+ | `highLabel` | "Very likely" |
63
+ | `commentLabel` | "What is the main reason for your score? (optional)" |
64
+ | `sendLabel` | "Send" |
65
+ | `notNowLabel` | "Not now" |
66
+ | `thanks` | "Thanks for your feedback." |
67
+
68
+ ### `<FeedbackButton />`
69
+
70
+ A button that opens a drawer with one comment box. Send posts the comment with the page it was
71
+ written on, then thanks the person. The page path is read when the person sends, unless `pagePath`
72
+ is given.
73
+
74
+ ```jsx
75
+ <FeedbackButton label="Feedback" />
76
+ ```
77
+
78
+ | Prop | Default |
79
+ |---|---|
80
+ | `label` | "Feedback" |
81
+ | `title` | "Send us feedback" |
82
+ | `placeholder` | "Tell us what works and what does not" |
83
+ | `sendLabel` | "Send" |
84
+ | `thanks` | "Thanks, we read every message." |
85
+ | `pagePath` | the current `window.location.pathname` at send time |
86
+
87
+ ## The wire contract
88
+
89
+ The prefix defaults to `/api/insights/feedback`. Errors are a JSON body `{ code, message }`.
90
+
91
+ | Method and route | Request body | Response |
92
+ |---|---|---|
93
+ | `GET {prefix}/nps` | none | `{ due, trigger }` |
94
+ | `POST {prefix}/nps` | `{ trigger, score: number or null, comment?: string or null }` | 204. A `score` of `null` is a permanent "not now". |
95
+ | `POST {prefix}/comments` | `{ comment, pagePath?: string or null }` | 204 |
96
+
97
+ ## The X-Webority-Surface header
98
+
99
+ Every POST sends the header `X-Webority-Surface: Web`, so the server can tell the portal's answers
100
+ from the other surfaces'. The GET does not send it.
101
+
102
+ ## Server side
103
+
104
+ The server side is Webority.Insights.AspNetCore's `MapWeborityInsightsFeedbackEndpoints`, which maps
105
+ the three routes above under the product's prefix.
@@ -0,0 +1 @@
1
+ import{AppAlert as e,AppButton as t,AppCard as n,AppDrawer as r,AppScoreScale as i,AppTextarea as a}from"@webority/ui-react";import{createContext as o,useContext as s,useEffect as c,useMemo as l,useRef as u,useState as d}from"react";import{Fragment as f,jsx as p,jsxs as m}from"react/jsx-runtime";var h=`/api/insights/feedback`,g=`Something went wrong. Please try again.`,_={headers:{"X-Webority-Surface":`Web`}},v=class extends Error{constructor(e,t,n){super(t),this.name=`InsightsFeedbackApiError`,this.code=e,this.status=n}};async function y(e){if(e&&typeof Blob<`u`&&e instanceof Blob)try{return JSON.parse(await e.text())}catch{return null}if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e??null}async function b(e){if(e instanceof v)return e;let t=e?.response;if(!t)return new v(`network`,g);let n=await y(t.data);return new v(typeof n?.code==`string`&&n.code?n.code:`unknown`,typeof n?.message==`string`&&n.message?n.message:g,t.status)}async function x(e){try{return(await e()).data}catch(e){throw await b(e)}}function S(e,t={}){let n=(t.prefix??h).replace(/\/+$/,``);return{getNps:()=>x(()=>e.get(`${n}/nps`)),answerNps:t=>x(()=>e.post(`${n}/nps`,t,_)),leaveComment:t=>x(()=>e.post(`${n}/comments`,t,_))}}var C=o(null);function w(){let e=s(C);if(!e)throw Error(`InsightsFeedbackProvider is missing.`);return e.client}var T=1e3;function E({label:n=`Feedback`,title:i=`Send us feedback`,placeholder:o=`Tell us what works and what does not`,sendLabel:s=`Send`,thanks:c=`Thanks, we read every message.`,pagePath:l}){let u=w(),[h,g]=d(!1),[_,v]=d(``),[y,b]=d(!1),[x,S]=d(!1),[C,E]=d(null);function D(){S(!1),E(null),g(!0)}async function O(){b(!0),E(null);try{await u.leaveComment({comment:_.trim(),pagePath:l??window.location.pathname}),v(``),S(!0)}catch(e){E(e)}finally{b(!1)}}let k=x?null:p(`div`,{className:`d-flex justify-content-end w-100`,children:p(t,{variant:`primary`,onClick:O,disabled:_.trim()===``||y,loading:y,children:s})});return m(f,{children:[p(t,{variant:`outline`,onClick:D,children:n}),p(r,{open:h,onClose:()=>g(!1),title:i,footer:k,children:x?p(`p`,{role:`status`,className:`mb-0`,children:c}):m(f,{children:[p(a,{"aria-label":i,placeholder:o,rows:5,maxLength:T,value:_,disabled:y,onChange:e=>v(e.target.value)}),C&&p(e,{variant:`danger`,children:C.message})]})})]})}function D({client:e,children:t}){let n=l(()=>({client:e}),[e]);return p(C.Provider,{value:n,children:t})}var O=1e3;function k({question:r=`How likely are you to recommend us to a colleague?`,lowLabel:o=`Not likely`,highLabel:s=`Very likely`,commentLabel:l=`What is the main reason for your score? (optional)`,sendLabel:f=`Send`,notNowLabel:h=`Not now`,thanks:g=`Thanks for your feedback.`}){let _=w(),[v,y]=d(`loading`),[b,x]=d(null),[S,C]=d(null),[T,E]=d(``),[D,k]=d(!1),[A,j]=d(null),M=u(!1);c(()=>{M.current||(M.current=!0,_.getNps().then(e=>{e?.due?(x(e.trigger),y(`asking`)):y(`closed`)},()=>y(`closed`)))},[_]);async function N(e){k(!0),j(null);try{return await _.answerNps({trigger:b,...e}),!0}catch(e){return j(e),!1}finally{k(!1)}}async function P(){let e=T.trim();await N({score:S,comment:e||null})&&y(`thanked`)}async function F(){await N({score:null})&&y(`closed`)}return v===`loading`||v===`closed`?null:v===`thanked`?p(n,{children:p(`p`,{role:`status`,className:`mb-0`,children:g})}):m(n,{children:[p(i,{name:`nps-score`,value:S,onChange:C,min:0,max:10,label:r,lowLabel:o,highLabel:s,disabled:D}),S!==null&&p(a,{label:l,rows:2,maxLength:O,value:T,disabled:D,onChange:e=>E(e.target.value)}),A&&p(e,{variant:`danger`,children:A.message}),m(`div`,{className:`d-flex gap-2 mt-3`,children:[p(t,{variant:`primary`,onClick:P,disabled:S===null||D,loading:D,children:f}),p(t,{variant:`secondary`,onClick:F,disabled:D,children:h})]})]})}export{E as FeedbackButton,v as InsightsFeedbackApiError,D as InsightsFeedbackProvider,k as NpsPrompt,S as createInsightsFeedbackClient};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A button that opens a drawer with one box for a comment. The comment is sent with the current
3
+ * page path, unless `pagePath` is given.
4
+ *
5
+ * @param {object} props
6
+ * @param {string} [props.label="Feedback"] The button text.
7
+ * @param {string} [props.title="Send us feedback"] The drawer's heading.
8
+ * @param {string} [props.placeholder="Tell us what works and what does not"]
9
+ * @param {string} [props.sendLabel="Send"]
10
+ * @param {string} [props.thanks="Thanks, we read every message."]
11
+ * @param {string|null} [props.pagePath] The page the comment is about. Defaults to the current path, read when sent.
12
+ */
13
+ export default function FeedbackButton({ label, title, placeholder, sendLabel, thanks, pagePath, }: {
14
+ label?: string;
15
+ title?: string;
16
+ placeholder?: string;
17
+ sendLabel?: string;
18
+ thanks?: string;
19
+ pagePath?: string | null;
20
+ }): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Makes a feedback client available to the NPS question and the feedback button below it.
3
+ *
4
+ * @param {object} props
5
+ * @param {import('./client.js').InsightsFeedbackClient} props.client From `createInsightsFeedbackClient(http)`.
6
+ * @param {import('react').ReactNode} props.children
7
+ */
8
+ export default function InsightsFeedbackProvider({ client, children }: {
9
+ client: import('./client.js').InsightsFeedbackClient;
10
+ children: import('react').ReactNode;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The one "how likely are you to recommend us" question. It asks once per mount, and only when
3
+ * the server says it is due. Send posts the score and an optional comment; Not now is a permanent
4
+ * answer the server records, so the question does not come back.
5
+ *
6
+ * @param {object} props
7
+ * @param {string} [props.question="How likely are you to recommend us to a colleague?"]
8
+ * @param {string} [props.lowLabel="Not likely"]
9
+ * @param {string} [props.highLabel="Very likely"]
10
+ * @param {string} [props.commentLabel="What is the main reason for your score? (optional)"]
11
+ * @param {string} [props.sendLabel="Send"]
12
+ * @param {string} [props.notNowLabel="Not now"]
13
+ * @param {string} [props.thanks="Thanks for your feedback."]
14
+ */
15
+ export default function NpsPrompt({ question, lowLabel, highLabel, commentLabel, sendLabel, notNowLabel, thanks, }: {
16
+ question?: string;
17
+ lowLabel?: string;
18
+ highLabel?: string;
19
+ commentLabel?: string;
20
+ sendLabel?: string;
21
+ notNowLabel?: string;
22
+ thanks?: string;
23
+ }): import("react").JSX.Element;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A failed feedback request. `message` is written for the person and is safe to show as-is.
3
+ */
4
+ export declare class InsightsFeedbackApiError extends Error {
5
+ code: string;
6
+ status: number;
7
+ /**
8
+ * @param {string} code Machine-readable reason from the API, or "network" when no response arrived.
9
+ * @param {string} message The sentence to show the person.
10
+ * @param {number} [status] HTTP status, when a response arrived.
11
+ */
12
+ constructor(code: string, message: string, status?: number);
13
+ }
14
+ export type InsightsHttp = {
15
+ get: (url: string, config?: object) => Promise<{
16
+ data: any;
17
+ }>;
18
+ post: (url: string, data?: any, config?: object) => Promise<{
19
+ data: any;
20
+ }>;
21
+ };
22
+ export type NpsState = {
23
+ /**
24
+ * Whether the question should be asked now.
25
+ */
26
+ due: boolean;
27
+ /**
28
+ * The server's reason for asking; sent back with the answer.
29
+ */
30
+ trigger: string;
31
+ };
32
+ export type NpsAnswer = {
33
+ /**
34
+ * The `trigger` the question was asked with.
35
+ */
36
+ trigger: string;
37
+ /**
38
+ * 0 to 10, or null for a permanent "not now".
39
+ */
40
+ score: number | null;
41
+ comment?: string | null;
42
+ };
43
+ export type CommentInput = {
44
+ comment: string;
45
+ /**
46
+ * The page the comment is about; the component sends the current path.
47
+ */
48
+ pagePath?: string | null;
49
+ };
50
+ export type InsightsFeedbackClient = {
51
+ getNps: () => Promise<NpsState>;
52
+ answerNps: (body: NpsAnswer) => Promise<void>;
53
+ leaveComment: (body: CommentInput) => Promise<void>;
54
+ };
55
+ /**
56
+ * @typedef {object} InsightsHttp An axios-like HTTP client that already carries the bearer token.
57
+ * @property {(url: string, config?: object) => Promise<{ data: any }>} get
58
+ * @property {(url: string, data?: any, config?: object) => Promise<{ data: any }>} post
59
+ */
60
+ /**
61
+ * @typedef {object} NpsState What the server says about the question for this person.
62
+ * @property {boolean} due Whether the question should be asked now.
63
+ * @property {string} trigger The server's reason for asking; sent back with the answer.
64
+ */
65
+ /**
66
+ * @typedef {object} NpsAnswer
67
+ * @property {string} trigger The `trigger` the question was asked with.
68
+ * @property {number|null} score 0 to 10, or null for a permanent "not now".
69
+ * @property {string|null} [comment]
70
+ */
71
+ /**
72
+ * @typedef {object} CommentInput
73
+ * @property {string} comment
74
+ * @property {string|null} [pagePath] The page the comment is about; the component sends the current path.
75
+ */
76
+ /**
77
+ * @typedef {object} InsightsFeedbackClient
78
+ * @property {() => Promise<NpsState>} getNps
79
+ * @property {(body: NpsAnswer) => Promise<void>} answerNps
80
+ * @property {(body: CommentInput) => Promise<void>} leaveComment
81
+ */
82
+ /**
83
+ * Binds the feedback HTTP contract to the product's own authenticated client.
84
+ *
85
+ * @param {import('axios').AxiosInstance} http The product's signed-in axios client (web-core userClient).
86
+ * @param {{ prefix?: string }} [options] `prefix` overrides the API prefix, which defaults to /api/insights/feedback.
87
+ * @returns {InsightsFeedbackClient}
88
+ */
89
+ export declare function createInsightsFeedbackClient(http: import('axios').AxiosInstance, options?: {
90
+ prefix?: string;
91
+ }): InsightsFeedbackClient;
@@ -0,0 +1,10 @@
1
+ /** @type {import('react').Context<{ client: import('./client.js').InsightsFeedbackClient } | null>} */
2
+ export declare const InsightsFeedbackContext: import('react').Context<{
3
+ client: import('./client.js').InsightsFeedbackClient;
4
+ } | null>;
5
+ /**
6
+ * The feedback client the nearest InsightsFeedbackProvider holds. Internal to the package.
7
+ *
8
+ * @returns {import('./client.js').InsightsFeedbackClient}
9
+ */
10
+ export declare function useInsightsFeedbackClient(): import('./client.js').InsightsFeedbackClient;
@@ -0,0 +1,4 @@
1
+ export { createInsightsFeedbackClient, InsightsFeedbackApiError } from "./client.js";
2
+ export { default as FeedbackButton } from "./FeedbackButton.jsx";
3
+ export { default as InsightsFeedbackProvider } from "./InsightsFeedbackProvider.jsx";
4
+ export { default as NpsPrompt } from "./NpsPrompt.jsx";
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@webority/insights-react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.65.0",
4
+ "description": "Webority product feedback for React portals: the NPS question and a feedback button that post to the product's Webority.Insights feedback API, built on @webority/ui-react.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "!dist/**/__tests__",
14
+ "!dist/**/*.test.*",
15
+ "!dist/**/*.map"
16
+ ],
17
+ "main": "./dist/insights-react.js",
18
+ "module": "./dist/insights-react.js",
19
+ "types": "./dist/types/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/types/index.d.ts",
23
+ "default": "./dist/insights-react.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "sideEffects": false,
28
+ "peerDependencies": {
29
+ "bootstrap": "5.3.3",
30
+ "react": "^19.0.0",
31
+ "react-bootstrap": "^2.10.0",
32
+ "react-dom": "^19.0.0",
33
+ "@webority/ui-react": "0.65.0"
34
+ },
35
+ "devDependencies": {
36
+ "@testing-library/dom": "10.4.2",
37
+ "@testing-library/jest-dom": "7.0.1",
38
+ "@testing-library/react": "^16.3.0",
39
+ "@testing-library/user-event": "^14.6.1",
40
+ "@types/react": "^19.2.17",
41
+ "@vitejs/plugin-react": "^6.0.1",
42
+ "@vitest/coverage-v8": "5.0.1",
43
+ "bootstrap": "5.3.3",
44
+ "jsdom": "30.1.1",
45
+ "react": "^19.2.6",
46
+ "react-bootstrap": "^2.10.6",
47
+ "react-dom": "^19.2.6",
48
+ "typescript": "^7.0.2",
49
+ "vite": "^8.0.12",
50
+ "vitest": "5.0.1",
51
+ "@webority/ui-react": "0.65.0"
52
+ },
53
+ "scripts": {
54
+ "build": "vite build && tsc -p tsconfig.json",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage"
58
+ }
6
59
  }