Skip to content

Commit 2c9a98d

Browse files
Adding feedback feature
1 parent bbb4a26 commit 2c9a98d

File tree

5 files changed

+235
-3
lines changed

5 files changed

+235
-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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@
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+
"feedbackButtonInfo": "Give us your feedback",
103+
"feedbackPlaceholder": "Please let us know what you think about our application",
104+
"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.",
105+
"feedbackThanks": "Thank you for your feedback!"
99106
},
100107
"CreateProjectDialog": {
101108
"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: 187 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,58 @@ 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';
2240

2341
export function ShellBarComponent() {
2442
const auth = useAuth();
2543
const profilePopoverRef = useRef<PopoverDomRef>(null);
2644
const betaPopoverRef = useRef<PopoverDomRef>(null);
45+
const feedbackPopoverRef = useRef<PopoverDomRef>(null);
2746
const [profilePopoverOpen, setProfilePopoverOpen] = useState(false);
2847
const [betaPopoverOpen, setBetaPopoverOpen] = useState(false);
48+
const [feedbackPopoverOpen, setFeedbackPopoverOpen] = useState(false);
49+
const [rating, setRating] = useState(0);
50+
const [feedbackMessage, setFeedbackMessage] = useState('');
51+
const [feedbackSent, setFeedbackSent] = useState(false);
52+
2953
const betaButtonRef = useRef<ButtonDomRef>(null);
3054

55+
const { user } = useAuth();
56+
3157
const onProfileClick = (
3258
e: Ui5CustomEvent<ShellBarDomRef, ShellBarProfileClickEventDetail>,
3359
) => {
@@ -42,6 +68,45 @@ export function ShellBarComponent() {
4268
}
4369
};
4470

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

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

0 commit comments

Comments
 (0)