diff --git a/.env.template b/.env.template index 5a864be6..137c9d12 100644 --- a/.env.template +++ b/.env.template @@ -15,3 +15,6 @@ API_BACKEND_URL= # Replace this value with a strong, randomly generated string (at least 32 characters). # Example for generation in Node.js: require('crypto').randomBytes(32).toString('hex') COOKIE_SECRET= + +FEEDBACK_SLACK_URL= +FEEDBACK_URL_LINK= diff --git a/public/locales/en.json b/public/locales/en.json index 63ff429e..023156b6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -95,7 +95,15 @@ }, "ShellBar": { "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!", - "signOutButton": "Sign Out" + "signOutButton": "Sign Out", + "feedbackMessageLabel": "Message", + "feedbackRatingLabel": "Rating", + "feedbackHeader": "Your feedback", + "feedbackButton": "Send feedback", + "feedbackButtonInfo": "Give us your feedback", + "feedbackPlaceholder": "Please let us know what you think about our application", + "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.", + "feedbackThanks": "Thank you for your feedback!" }, "CreateProjectDialog": { "toastMessage": "Project creation triggered. The list will refresh automatically once completed." diff --git a/server/config/env.js b/server/config/env.js index c137d93d..58d62169 100644 --- a/server/config/env.js +++ b/server/config/env.js @@ -13,6 +13,8 @@ const schema = { POST_LOGIN_REDIRECT: { type: "string" }, COOKIE_SECRET: { type: "string" }, API_BACKEND_URL: { type: "string" }, + FEEDBACK_SLACK_URL: { type: "string" }, + FEEDBACK_URL_LINK: { type: "string" }, // System variables NODE_ENV: { type: "string", enum: ["development", "production"] }, diff --git a/server/routes/feedback.js b/server/routes/feedback.js new file mode 100644 index 00000000..005a5c4b --- /dev/null +++ b/server/routes/feedback.js @@ -0,0 +1,35 @@ +import fetch from 'node-fetch'; +import fp from "fastify-plugin"; + +async function feedbackRoute(fastify) { + const { FEEDBACK_SLACK_URL } = fastify.config; + + fastify.post('/feedback', async (request, reply) => { + const { message, rating, user, environment } = request.body; + + if (!message || !rating || !user || !environment) { + return reply.status(400).send({ error: 'Missing required fields' }); + } + + try { + const res = await fetch(FEEDBACK_SLACK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message, rating, user, environment }), + }); + + if (!res.ok) { + return reply.status(500).send({ error: 'Slack API error' }); + } + return reply.send({ message: res, }); + } catch (err) { + fastify.log.error('Slack error:', err); + return reply.status(500).send({ error: 'Request failed' }); + } + }); +} + +export default fp(feedbackRoute); + diff --git a/src/components/Core/ShellBar.tsx b/src/components/Core/ShellBar.tsx index df6cc3bb..4098d6f0 100644 --- a/src/components/Core/ShellBar.tsx +++ b/src/components/Core/ShellBar.tsx @@ -2,32 +2,59 @@ import { Avatar, Button, ButtonDomRef, + Form, + FormGroup, + FormItem, Icon, + Label, List, ListItemStandard, Popover, PopoverDomRef, + RatingIndicator, ShellBar, ShellBarDomRef, + ShellBarItem, + ShellBarItemDomRef, + TextArea, + TextAreaDomRef, Ui5CustomEvent, } from '@ui5/webcomponents-react'; import { useAuth } from '../../spaces/onboarding/auth/AuthContext.tsx'; -import { RefObject, useEffect, useRef, useState } from 'react'; +import { + Dispatch, + RefObject, + SetStateAction, + useEffect, + useRef, + useState, +} from 'react'; import { ShellBarProfileClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBar.js'; import PopoverPlacement from '@ui5/webcomponents/dist/types/PopoverPlacement.js'; import { useTranslation } from 'react-i18next'; import { generateInitialsForEmail } from '../Helper/generateInitialsForEmail.ts'; import styles from './ShellBar.module.css'; import { ThemingParameters } from '@ui5/webcomponents-react-base'; +import { ShellBarItemClickEventDetail } from '@ui5/webcomponents-fiori/dist/ShellBarItem.js'; +import { t } from 'i18next'; + +type UI5RatingIndicatorElement = HTMLElement & { value: number }; export function ShellBarComponent() { const auth = useAuth(); const profilePopoverRef = useRef(null); const betaPopoverRef = useRef(null); + const feedbackPopoverRef = useRef(null); const [profilePopoverOpen, setProfilePopoverOpen] = useState(false); const [betaPopoverOpen, setBetaPopoverOpen] = useState(false); + const [feedbackPopoverOpen, setFeedbackPopoverOpen] = useState(false); + const [rating, setRating] = useState(0); + const [feedbackMessage, setFeedbackMessage] = useState(''); + const [feedbackSent, setFeedbackSent] = useState(false); const betaButtonRef = useRef(null); + const { user } = useAuth(); + const onProfileClick = ( e: Ui5CustomEvent, ) => { @@ -42,6 +69,45 @@ export function ShellBarComponent() { } }; + const onFeedbackClick = ( + e: Ui5CustomEvent, + ) => { + feedbackPopoverRef.current!.opener = e.detail.targetRef; + setFeedbackPopoverOpen(!feedbackPopoverOpen); + }; + + const onFeedbackMessageChange = ( + event: Ui5CustomEvent< + TextAreaDomRef, + { value: string; previousValue: string } + >, + ) => { + const newValue = event.target.value; + setFeedbackMessage(newValue); + }; + + async function onFeedbackSent() { + const payload = { + message: feedbackMessage, + rating: rating.toString(), + user: user?.email, + environment: window.location.hostname, + }; + try { + await fetch('/api/feedback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } catch (err) { + console.log(err); + } finally { + setFeedbackSent(true); + } + } + useEffect(() => { const shellbar = document.querySelector('ui5-shellbar'); const el = shellbar?.shadowRoot?.querySelector( @@ -82,7 +148,13 @@ export function ShellBarComponent() { } onProfileClick={onProfileClick} - /> + > + + + ); } @@ -163,3 +246,106 @@ const BetaPopover = ({ ); }; + +const FeedbackPopover = ({ + open, + setOpen, + popoverRef, + setRating, + rating, + onFeedbackSent, + feedbackMessage, + onFeedbackMessageChange, + feedbackSent, +}: { + open: boolean; + setOpen: (arg0: boolean) => void; + popoverRef: RefObject; + setRating: Dispatch>; + rating: number; + onFeedbackSent: () => void; + feedbackMessage: string; + onFeedbackMessageChange: ( + event: Ui5CustomEvent< + TextAreaDomRef, + { + value: string; + previousValue: string; + } + >, + ) => void; + feedbackSent: boolean; +}) => { + const { t } = useTranslation(); + + const onRatingChange = ( + event: Event & { target: UI5RatingIndicatorElement }, + ) => { + setRating(event.target.value); + }; + + return ( + <> + setOpen(false)} + > +
+ {!feedbackSent ? ( +
+ + + {t('ShellBar.feedbackRatingLabel')} + + } + > + + + + {t('ShellBar.feedbackMessageLabel')} + + } + > +