@learningpool/ui 2.3.6 → 2.4.1

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.
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ interface FeedbackRatingProps {
3
+ /** Required: Unique identifier for the feature this feedback is associated with
4
+ * Example: enrolment-reasons-sidebar
5
+ */
6
+ featureId: string;
7
+ /** Required: Product identifier to differentiate between different applications in the results
8
+ * Example: LRS
9
+ */
10
+ productId: string;
11
+ /**
12
+ * Required only when using the built-in Hub endpoint (i.e., when `token` is provided and `submitRating` is not).
13
+ * Used as the x-api-key header for authenticating through AWS Gateway for the Hub feedback endpoint.
14
+ * Example: 'abcd1234efgh5678ijkl9012mnop3456'
15
+ */
16
+ apiKey?: string;
17
+ /**
18
+ * Auth token for Hub endpoint, if no token is provided then feedback won't be logged.
19
+ * If not providing, please provide your own `submitRating` to handle Hub API
20
+ */
21
+ token?: string;
22
+ /** Optional callback invoked when the user selects a rating.
23
+ * If provided, this overrides the built‑in Hub submission logic.
24
+ */
25
+ submitRating?: (rating: number, featureId: string) => Promise<void> | void;
26
+ /** Optional message shown next to the rating control
27
+ * Defaults to "We've made some changes. Share your feedback with a star rating."
28
+ */
29
+ message?: React.ReactNode;
30
+ /**
31
+ * Optional user email to forward to the Hub endpoint (validated as an email by the API).
32
+ */
33
+ email?: string;
34
+ /**
35
+ * Optional Base URL for the external feedback form (e.g., Google Form).
36
+ * The component will append prefill parameters (rating, featureId, productId, pageUrl).
37
+ * Defaults to DEFAULT_FEEDBACK_FORM_URL
38
+ */
39
+ feedbackFormUrl?: string;
40
+ /**
41
+ * Optional Base URL for the Hub, used to build feedback links, so that an environment variable can be used in staging environments
42
+ * Example: https://api.home-staging.learningpool.com/
43
+ * Note: must include trailing slash
44
+ */
45
+ hubBaseUrl?: string;
46
+ /**
47
+ * Customer identifier required by the Hub endpoint when using the built-in submit.
48
+ * If omitted, an empty string will be sent in the payload. Provide your own `submitRating` to override if you need custom logic.
49
+ * Example: org-12345
50
+ */
51
+ customerId?: string;
52
+ /**
53
+ * Optional expiry for showing the component.
54
+ * When the current time is beyond this timestamp, the component will render null and not show.
55
+ * Accepts an ISO 8601 date-time string (e.g., '2026-03-01T00:00:00Z').
56
+ */
57
+ expiresAt?: string;
58
+ /**
59
+ * Optional page URL override used in payloads and links. If omitted, the component will use the current window location when available.
60
+ */
61
+ pageUrl?: string;
62
+ /**
63
+ * Optional test id prefix to help with testing across apps
64
+ */
65
+ testIdPrefix?: string;
66
+ /**
67
+ * Optional metadata object to include with the Hub payload.
68
+ * Keys must be non-empty strings (<=255). Values may be string (<=255), number, or boolean. Max ~10 entries enforced server-side.
69
+ */
70
+ metadata?: Record<string, string | number | boolean>;
71
+ }
72
+ /**
73
+ * A dismissible feedback bar with 5-star rating.
74
+ * - Saves rating and dismiss state to localStorage, scoped by featureId
75
+ * - Calls `submitRating` on star click (or a placeholder POST)
76
+ * - After rating, shows a link to provide more feedback (opens in new tab)
77
+ */
78
+ export default function FeedbackRating({ featureId, productId, apiKey, token, submitRating, message, email, feedbackFormUrl, hubBaseUrl, customerId, expiresAt, pageUrl, testIdPrefix, metadata }: FeedbackRatingProps): JSX.Element | null;
79
+ export {};
@@ -0,0 +1,201 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from 'react';
3
+ import CloseIcon from '@mui/icons-material/Close';
4
+ import { Alert, Box, Button, Collapse, Link, Rating, Stack, Typography, useTheme } from '@mui/material';
5
+ import { Constants } from '../../stream/AppSwitcher/constants';
6
+ import { defaultMessages } from '../../../lang/en-us';
7
+ const LOCAL_STORAGE_PREFIX = 'admin-feedback';
8
+ const DISMISS_ANIMATION_DELAY_MS = 300;
9
+ const GOOGLE_FORM_CONFIG = {
10
+ BASE_URL: 'https://docs.google.com/forms/d/e/1FAIpQLSfKJTrprcJcAKYOykMmx56jC7ajuu9LIq52Uc1u2hqnLmgt0w/viewform',
11
+ PARAMS: {
12
+ usp: 'pp_url',
13
+ rating: 'entry.436394498',
14
+ email: 'entry.967663492',
15
+ featureId: 'entry.1339542182',
16
+ orgAlias: 'entry.444516590',
17
+ productId: 'entry.398489558',
18
+ pageUrl: 'entry.1804127593'
19
+ }
20
+ };
21
+ const DEFAULT_FEEDBACK_FORM_URL = GOOGLE_FORM_CONFIG.BASE_URL;
22
+ const defaultMessageNode = (_jsxs(_Fragment, { children: [_jsx("strong", { children: defaultMessages['feedback-message-prefix'] }), ' ', defaultMessages['feedback-message-suffix']] }));
23
+ /**
24
+ * A dismissible feedback bar with 5-star rating.
25
+ * - Saves rating and dismiss state to localStorage, scoped by featureId
26
+ * - Calls `submitRating` on star click (or a placeholder POST)
27
+ * - After rating, shows a link to provide more feedback (opens in new tab)
28
+ */
29
+ export default function FeedbackRating({ featureId, productId, apiKey, token, submitRating, message, email, feedbackFormUrl, hubBaseUrl, customerId, expiresAt, pageUrl, testIdPrefix, metadata }) {
30
+ const theme = useTheme();
31
+ const localStorageKeys = useMemo(() => ({
32
+ rating: `${LOCAL_STORAGE_PREFIX}-rating-${featureId}`,
33
+ dismissed: `${LOCAL_STORAGE_PREFIX}-dismissed-${featureId}`
34
+ }), [featureId]);
35
+ const [open, setOpen] = useState(true);
36
+ const [isDismissed, setIsDismissed] = useState(false);
37
+ const [rating, setRating] = useState(null);
38
+ const hubFeedbackUrl = hubBaseUrl || Constants.BaseUrl;
39
+ const userMessage = message ?? defaultMessageNode;
40
+ const resolvedPageUrl = pageUrl ?? (typeof window !== 'undefined' ? window.location.href : '');
41
+ const testIdPrefixValue = testIdPrefix ?? 'feedback-rating';
42
+ const buildDefaultFeedbackUrl = useCallback((value) => {
43
+ const baseFormUrl = feedbackFormUrl || DEFAULT_FEEDBACK_FORM_URL;
44
+ // Use Google Form entry IDs for prefill
45
+ const params = new URLSearchParams({
46
+ [GOOGLE_FORM_CONFIG.PARAMS.usp]: 'pp_url',
47
+ [GOOGLE_FORM_CONFIG.PARAMS.email]: email || '',
48
+ [GOOGLE_FORM_CONFIG.PARAMS.featureId]: featureId,
49
+ [GOOGLE_FORM_CONFIG.PARAMS.orgAlias]: customerId || '',
50
+ [GOOGLE_FORM_CONFIG.PARAMS.productId]: productId,
51
+ [GOOGLE_FORM_CONFIG.PARAMS.pageUrl]: resolvedPageUrl
52
+ });
53
+ if (value != null && value >= 1 && value <= 5) {
54
+ params.append(GOOGLE_FORM_CONFIG.PARAMS.rating, value.toString());
55
+ }
56
+ return `${baseFormUrl}?${params.toString()}`;
57
+ }, [feedbackFormUrl, featureId, customerId, productId, resolvedPageUrl, email]);
58
+ const expiryTimestamp = useMemo(() => {
59
+ if (expiresAt == null) {
60
+ return undefined;
61
+ }
62
+ const parsed = Date.parse(expiresAt);
63
+ return Number.isNaN(parsed) ? undefined : parsed;
64
+ }, [expiresAt]);
65
+ const isExpired = useMemo(() => {
66
+ if (expiryTimestamp == null)
67
+ return false;
68
+ return Date.now() >= expiryTimestamp;
69
+ }, [expiryTimestamp]);
70
+ useEffect(() => {
71
+ try {
72
+ const ls = typeof window !== 'undefined' ? window.localStorage : undefined;
73
+ const storedDismissed = ls?.getItem(localStorageKeys.dismissed);
74
+ const storedRating = ls?.getItem(localStorageKeys.rating);
75
+ if (storedDismissed === 'true') {
76
+ setIsDismissed(true);
77
+ }
78
+ if (storedRating) {
79
+ const parsed = Number(storedRating);
80
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 5) {
81
+ setRating(parsed);
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // ignore localStorage errors
87
+ }
88
+ }, [localStorageKeys.dismissed, localStorageKeys.rating]);
89
+ const saveRating = useCallback((value) => {
90
+ try {
91
+ const ls = typeof window !== 'undefined' ? window.localStorage : undefined;
92
+ ls?.setItem(localStorageKeys.rating, String(value));
93
+ }
94
+ catch {
95
+ // ignore localStorage errors
96
+ }
97
+ }, [localStorageKeys.rating]);
98
+ const dismiss = useCallback(() => {
99
+ try {
100
+ const ls = typeof window !== 'undefined' ? window.localStorage : undefined;
101
+ ls?.setItem(localStorageKeys.dismissed, 'true');
102
+ }
103
+ catch {
104
+ // ignore localStorage errors
105
+ }
106
+ setIsDismissed(true);
107
+ }, [localStorageKeys.dismissed]);
108
+ const defaultSubmit = useCallback(async (value) => {
109
+ try {
110
+ if (!productId || !customerId) {
111
+ console.warn('[FeedbackRating] Missing productId or customerId; skipping default submit. Provide both or pass a custom submitRating.');
112
+ return;
113
+ }
114
+ if (!resolvedPageUrl) {
115
+ console.warn('[FeedbackRating] pageUrl unavailable; skipping default submit.');
116
+ return;
117
+ }
118
+ if (!token || !apiKey) {
119
+ console.warn('[FeedbackRating] Invalid token or apiKey prop: feedback won\'t be stored.');
120
+ return;
121
+ }
122
+ const headers = {
123
+ 'Content-Type': 'application/json',
124
+ 'x-api-key': apiKey,
125
+ Authorization: token
126
+ };
127
+ const payload = {
128
+ rating: value,
129
+ featureId,
130
+ productId,
131
+ customerId,
132
+ pageUrl: resolvedPageUrl,
133
+ metadata: metadata ?? {}
134
+ };
135
+ if (email) {
136
+ payload.email = email;
137
+ }
138
+ const response = await fetch(`${hubFeedbackUrl}feedback`, {
139
+ method: 'POST',
140
+ headers,
141
+ body: JSON.stringify(payload)
142
+ });
143
+ if (response.ok === false) {
144
+ console.error('[FeedbackRating] Failed to submit feedback', await response.text());
145
+ }
146
+ }
147
+ catch (error) {
148
+ console.error('[FeedbackRating] Error submitting feedback', error);
149
+ }
150
+ }, [productId, customerId, featureId, hubFeedbackUrl, email, metadata, resolvedPageUrl, token, apiKey]);
151
+ const handleStarClick = useCallback(async (value) => {
152
+ setRating(value);
153
+ saveRating(value);
154
+ await (submitRating ? submitRating(value, featureId) : defaultSubmit(value));
155
+ }, [saveRating, submitRating, defaultSubmit, featureId]);
156
+ const handleDismiss = useCallback(() => {
157
+ setOpen(false);
158
+ setTimeout(() => {
159
+ dismiss();
160
+ }, DISMISS_ANIMATION_DELAY_MS);
161
+ }, [dismiss]);
162
+ if (isDismissed || isExpired) {
163
+ return null;
164
+ }
165
+ const feedbackUrl = buildDefaultFeedbackUrl(rating);
166
+ return (_jsx(Box, { sx: { marginBottom: theme.spacing(2) }, children: _jsx(Collapse, { in: open, children: _jsx(Alert, { severity: 'info', icon: false, sx: {
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ overflowX: 'hidden',
170
+ '& .MuiAlert-message': {
171
+ width: '100%',
172
+ minWidth: 0,
173
+ overflow: 'hidden',
174
+ display: 'block'
175
+ }
176
+ }, "data-testid": `${testIdPrefixValue}-alert-${featureId}`, action: _jsx(Button, { color: 'inherit', size: 'small', onClick: handleDismiss, "data-testid": `${testIdPrefixValue}-dismiss-${featureId}`, sx: { alignSelf: 'center', minWidth: 0, padding: 0 }, "aria-label": defaultMessages.dismiss, children: _jsx(CloseIcon, { fontSize: 'small' }) }), children: _jsxs(Box, { sx: {
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ justifyContent: 'space-between',
180
+ width: '100%',
181
+ gap: 2,
182
+ minWidth: 0
183
+ }, children: [_jsxs(Stack, { children: [_jsx(Typography, { variant: 'subtitle2', sx: {
184
+ flex: 1,
185
+ minWidth: 0,
186
+ whiteSpace: 'normal',
187
+ overflowWrap: 'anywhere',
188
+ wordBreak: 'break-word'
189
+ }, children: userMessage }), _jsx(Link, { href: feedbackUrl, target: '_blank', rel: 'noopener noreferrer', underline: 'hover', variant: 'body2', "data-testid": `${testIdPrefixValue}-more-${featureId}`, children: defaultMessages['feedback-message-more-text'] })] }), _jsx(Rating, { name: `feedback-rating-${featureId}`, value: rating, onChange: (_event, newValue) => {
190
+ if (newValue) {
191
+ handleStarClick(newValue);
192
+ }
193
+ }, sx: {
194
+ display: 'flex',
195
+ alignSelf: 'center',
196
+ alignItems: 'center',
197
+ '& .MuiRating-iconFilled': { color: theme.palette.info.main },
198
+ '& .MuiRating-iconHover': { color: theme.palette.info.light },
199
+ cursor: 'pointer'
200
+ }, "data-testid": `${testIdPrefixValue}-stars-${featureId}` })] }) }) }) }));
201
+ }
package/index.d.ts CHANGED
@@ -135,3 +135,4 @@ export { default as AppSwitcher } from './components/stream/AppSwitcher/AppSwitc
135
135
  export { default as Header } from './components/landmarks/Header/Header';
136
136
  export { default as MobileNavigation } from './components/navigation/MobileNavigation/MobileNavigation';
137
137
  export { default as VerticalNavigation } from './components/navigation/VerticalNavigation/VerticalNavigation';
138
+ export { default as FeedbackRating } from './components/feedback/FeedbackRating/FeedbackRating';
package/index.js CHANGED
@@ -146,3 +146,4 @@ export { default as AppSwitcher } from './components/stream/AppSwitcher/AppSwitc
146
146
  export { default as Header } from './components/landmarks/Header/Header';
147
147
  export { default as MobileNavigation } from './components/navigation/MobileNavigation/MobileNavigation';
148
148
  export { default as VerticalNavigation } from './components/navigation/VerticalNavigation/VerticalNavigation';
149
+ export { default as FeedbackRating } from './components/feedback/FeedbackRating/FeedbackRating';
package/lang/en-us.d.ts CHANGED
@@ -28,4 +28,8 @@ export const defaultMessages: {
28
28
  'help-center': string;
29
29
  'submit-feedback': string;
30
30
  'contact-us': string;
31
+ 'feedback-message-prefix': string;
32
+ 'feedback-message-suffix': string;
33
+ 'feedback-message-more-text': string;
34
+ dismiss: string;
31
35
  };
package/lang/en-us.js CHANGED
@@ -30,5 +30,10 @@ export const defaultMessages = {
30
30
  support: 'Support',
31
31
  'help-center': 'Help Centre',
32
32
  'submit-feedback': 'Submit Feedback',
33
- 'contact-us': 'Contact Us'
33
+ 'contact-us': 'Contact Us',
34
+ // Feedback Rating
35
+ 'feedback-message-prefix': "We've made some changes.",
36
+ 'feedback-message-suffix': 'Share your feedback with a star rating.',
37
+ 'feedback-message-more-text': 'Give Further Feedback',
38
+ dismiss: 'Dismiss'
34
39
  };
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "components",
10
10
  "ui"
11
11
  ],
12
- "version": "2.3.6",
12
+ "version": "2.4.1",
13
13
  "private": false,
14
14
  "main": "index.js",
15
15
  "module": "index.js",