Skip to content

Commit adb2623

Browse files
feat: adding feedback feature
1 parent 2fda310 commit adb2623

File tree

5 files changed

+237
-3
lines changed

5 files changed

+237
-3
lines changed

.env.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ API_BACKEND_URL=
1515
# Replace this value with a strong, randomly generated string (at least 32 characters).
1616
# Example for generation in Node.js: require('crypto').randomBytes(32).toString('hex')
1717
COOKIE_SECRET=
18+
19+
FEEDBACK_SLACK_URL=
20+
FEEDBACK_URL_LINK=

public/locales/en.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,15 @@
9595
},
9696
"ShellBar": {
9797
"betaButtonDescription": "This web app is currently in Beta, and may not ready for productive use. We're actively improving the experience and would love your feedback — your input helps shape the future of the app!",
98-
"signOutButton": "Sign Out"
98+
"signOutButton": "Sign Out",
99+
"feedbackMessageLabel": "Message",
100+
"feedbackRatingLabel": "Rating",
101+
"feedbackHeader": "Your feedback",
102+
"feedbackButton": "Send feedback",
103+
"feedbackButtonInfo": "Give us your feedback",
104+
"feedbackPlaceholder": "Please let us know what you think about our application",
105+
"feedbackNotification": "*Slack notification with your email address will be shared with our Operations Team. If you have a special Feature request in mind, please create here.",
106+
"feedbackThanks": "Thank you for your feedback!"
99107
},
100108
"CreateProjectDialog": {
101109
"toastMessage": "Project creation triggered. The list will refresh automatically once completed."

server/config/env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const schema = {
1313
POST_LOGIN_REDIRECT: { type: "string" },
1414
COOKIE_SECRET: { type: "string" },
1515
API_BACKEND_URL: { type: "string" },
16+
FEEDBACK_SLACK_URL: { type: "string" },
17+
FEEDBACK_URL_LINK: { type: "string" },
1618

1719
// System variables
1820
NODE_ENV: { type: "string", enum: ["development", "production"] },

server/routes/feedback.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import fetch from 'node-fetch';
2+
import fp from "fastify-plugin";
3+
4+
async function feedbackRoute(fastify) {
5+
const { FEEDBACK_SLACK_URL } = fastify.config;
6+
7+
fastify.post('/feedback', async (request, reply) => {
8+
const { message, rating, user, environment } = request.body;
9+
10+
if (!message || !rating || !user || !environment) {
11+
return reply.status(400).send({ error: 'Missing required fields' });
12+
}
13+
14+
try {
15+
const res = await fetch(FEEDBACK_SLACK_URL, {
16+
method: 'POST',
17+
headers: {
18+
'Content-Type': 'application/json',
19+
},
20+
body: JSON.stringify({ message, rating, user, environment }),
21+
});
22+
23+
if (!res.ok) {
24+
return reply.status(500).send({ error: 'Slack API error' });
25+
}
26+
return reply.send({ message: res, });
27+
} catch (err) {
28+
fastify.log.error('Slack error:', err);
29+
return reply.status(500).send({ error: 'Request failed' });
30+
}
31+
});
32+
}
33+
34+
export default fp(feedbackRoute);
35+

src/components/Core/ShellBar.tsx

Lines changed: 188 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,59 @@ import {
22
Avatar,
33
Button,
44
ButtonDomRef,
5+
Form,
6+
FormGroup,
7+
FormItem,
58
Icon,
9+
Label,
610
List,
711
ListItemStandard,
812
Popover,
913
PopoverDomRef,
14+
RatingIndicator,
1015
ShellBar,
1116
ShellBarDomRef,
17+
ShellBarItem,
18+
ShellBarItemDomRef,
19+
TextArea,
20+
TextAreaDomRef,
1221
Ui5CustomEvent,
1322
} from '@ui5/webcomponents-react';
1423
import { useAuth } from '../../spaces/onboarding/auth/AuthContext.tsx';
15-
import { RefObject, useEffect, useRef, useState } from 'react';
24+
import {
25+
Dispatch,
26+
RefObject,
27+
SetStateAction,
28+
useEffect,
29+
useRef,
30+
useState,
31+
} from 'react';
1632
import { ShellBarProfileClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBar.js';
1733
import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js';
1834
import { useTranslation } from 'react-i18next';
1935
import { generateInitialsForEmail } from '../Helper/generateInitialsForEmail.ts';
2036
import styles from './ShellBar.module.css';
2137
import { ThemingParameters } from '@ui5/webcomponents-react-base';
38+
import { ShellBarItemClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBarItem.js';
39+
import { t } from 'i18next';
40+
41+
type UI5RatingIndicatorElement = HTMLElement & { value: number };
2242

2343
export function ShellBarComponent() {
2444
const auth = useAuth();
2545
const profilePopoverRef = useRef<PopoverDomRef>(null);
2646
const betaPopoverRef = useRef<PopoverDomRef>(null);
47+
const feedbackPopoverRef = useRef<PopoverDomRef>(null);
2748
const [profilePopoverOpen, setProfilePopoverOpen] = useState(false);
2849
const [betaPopoverOpen, setBetaPopoverOpen] = useState(false);
50+
const [feedbackPopoverOpen, setFeedbackPopoverOpen] = useState(false);
51+
const [rating, setRating] = useState(0);
52+
const [feedbackMessage, setFeedbackMessage] = useState('');
53+
const [feedbackSent, setFeedbackSent] = useState(false);
2954
const betaButtonRef = useRef<ButtonDomRef>(null);
3055

56+
const { user } = useAuth();
57+
3158
const onProfileClick = (
3259
e: Ui5CustomEvent<ShellBarDomRef, ShellBarProfileClickEventDetail>,
3360
) => {
@@ -42,6 +69,45 @@ export function ShellBarComponent() {
4269
}
4370
};
4471

72+
const onFeedbackClick = (
73+
e: Ui5CustomEvent<ShellBarItemDomRef, ShellBarItemClickEventDetail>,
74+
) => {
75+
feedbackPopoverRef.current!.opener = e.detail.targetRef;
76+
setFeedbackPopoverOpen(!feedbackPopoverOpen);
77+
};
78+
79+
const onFeedbackMessageChange = (
80+
event: Ui5CustomEvent<
81+
TextAreaDomRef,
82+
{ value: string; previousValue: string }
83+
>,
84+
) => {
85+
const newValue = event.target.value;
86+
setFeedbackMessage(newValue);
87+
};
88+
89+
async function onFeedbackSent() {
90+
const payload = {
91+
message: feedbackMessage,
92+
rating: rating.toString(),
93+
user: user?.email,
94+
environment: window.location.hostname,
95+
};
96+
try {
97+
await fetch('/api/feedback', {
98+
method: 'POST',
99+
headers: {
100+
'Content-Type': 'application/json',
101+
},
102+
body: JSON.stringify(payload),
103+
});
104+
} catch (err) {
105+
console.log(err);
106+
} finally {
107+
setFeedbackSent(true);
108+
}
109+
}
110+
45111
useEffect(() => {
46112
const shellbar = document.querySelector('ui5-shellbar');
47113
const el = shellbar?.shadowRoot?.querySelector(
@@ -82,7 +148,13 @@ export function ShellBarComponent() {
82148
</div>
83149
}
84150
onProfileClick={onProfileClick}
85-
/>
151+
>
152+
<ShellBarItem
153+
icon="feedback"
154+
text={t('ShellBar.feedbackNotification')}
155+
onClick={onFeedbackClick}
156+
/>
157+
</ShellBar>
86158

87159
<ProfilePopover
88160
open={profilePopoverOpen}
@@ -94,6 +166,17 @@ export function ShellBarComponent() {
94166
setOpen={setBetaPopoverOpen}
95167
popoverRef={betaPopoverRef}
96168
/>
169+
<FeedbackPopover
170+
open={feedbackPopoverOpen}
171+
setOpen={setFeedbackPopoverOpen}
172+
popoverRef={feedbackPopoverRef}
173+
setRating={setRating}
174+
rating={rating}
175+
feedbackMessage={feedbackMessage}
176+
feedbackSent={feedbackSent}
177+
onFeedbackSent={onFeedbackSent}
178+
onFeedbackMessageChange={onFeedbackMessageChange}
179+
/>
97180
</>
98181
);
99182
}
@@ -163,3 +246,106 @@ const BetaPopover = ({
163246
</Popover>
164247
);
165248
};
249+
250+
const FeedbackPopover = ({
251+
open,
252+
setOpen,
253+
popoverRef,
254+
setRating,
255+
rating,
256+
onFeedbackSent,
257+
feedbackMessage,
258+
onFeedbackMessageChange,
259+
feedbackSent,
260+
}: {
261+
open: boolean;
262+
setOpen: (arg0: boolean) => void;
263+
popoverRef: RefObject<PopoverDomRef | null>;
264+
setRating: Dispatch<SetStateAction<number>>;
265+
rating: number;
266+
onFeedbackSent: () => void;
267+
feedbackMessage: string;
268+
onFeedbackMessageChange: (
269+
event: Ui5CustomEvent<
270+
TextAreaDomRef,
271+
{
272+
value: string;
273+
previousValue: string;
274+
}
275+
>,
276+
) => void;
277+
feedbackSent: boolean;
278+
}) => {
279+
const { t } = useTranslation();
280+
281+
const onRatingChange = (
282+
event: Event & { target: UI5RatingIndicatorElement },
283+
) => {
284+
setRating(event.target.value);
285+
};
286+
287+
return (
288+
<>
289+
<Popover
290+
ref={popoverRef}
291+
placement={PopoverPlacement.Bottom}
292+
open={open}
293+
onClose={() => setOpen(false)}
294+
>
295+
<div
296+
style={{
297+
padding: '1rem',
298+
width: '250px',
299+
}}
300+
>
301+
{!feedbackSent ? (
302+
<Form headerText={t('ShellBar.feedbackHeader')}>
303+
<FormGroup>
304+
<FormItem
305+
labelContent={
306+
<Label style={{ color: 'black' }}>
307+
{t('ShellBar.feedbackRatingLabel')}
308+
</Label>
309+
}
310+
>
311+
<RatingIndicator
312+
value={rating}
313+
max={5}
314+
onChange={onRatingChange}
315+
/>
316+
</FormItem>
317+
<FormItem
318+
className="formAlignLabelStart"
319+
labelContent={
320+
<Label style={{ color: 'black' }}>
321+
{t('ShellBar.feedbackMessageLabel')}
322+
</Label>
323+
}
324+
>
325+
<TextArea
326+
value={feedbackMessage}
327+
placeholder={t('ShellBar.feedbackPlaceholder')}
328+
rows={5}
329+
onInput={onFeedbackMessageChange}
330+
/>
331+
</FormItem>
332+
<FormItem>
333+
<Button design="Emphasized" onClick={() => onFeedbackSent()}>
334+
{t('ShellBar.feedbackButton')}
335+
</Button>
336+
</FormItem>
337+
<FormItem>
338+
<Label style={{ color: 'gray' }}>
339+
{t('ShellBar.feedbackNotification')}
340+
</Label>
341+
</FormItem>
342+
</FormGroup>
343+
</Form>
344+
) : (
345+
<Label>{t('ShellBar.feedbackThanks')}</Label>
346+
)}
347+
</div>
348+
</Popover>
349+
</>
350+
);
351+
};

0 commit comments

Comments
 (0)