Skip to content

Commit 6e18e96

Browse files
authored
feat(userfeedback): User feedback client UI on sentry-docs site (#7761)
Refactored experimental user feedback from hackweek for the modal and feedback button. Removed the screenshot functionality, removed the title from the form and rearranged the form so the name and email are at the top. Relates to https://github.com/getsentry/team-replay/issues/170 <img width="1346" alt="image" src="https://github.com/getsentry/sentry-docs/assets/79684/43b0aecf-d202-4b46-b075-a9b0bd227162"> <img width="564" alt="image" src="https://github.com/getsentry/sentry-docs/assets/79684/ac0242b7-faee-4685-8fac-a61a010f9785">
1 parent 1b6ea55 commit 6e18e96

File tree

13 files changed

+741
-48
lines changed

13 files changed

+741
-48
lines changed

gatsby-browser.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import React from 'react';
22
import {GatsbyBrowser} from 'gatsby';
33

4+
import {FeedbackWidget} from 'sentry-docs/components/feedback/feedbackWidget';
45
import PageContext from 'sentry-docs/components/pageContext';
56

67
export const wrapPageElement: GatsbyBrowser['wrapPageElement'] = ({
78
element,
89
props: {pageContext},
9-
}) => <PageContext.Provider value={pageContext}>{element}</PageContext.Provider>;
10+
}) => <PageContext.Provider value={pageContext}>
11+
<FeedbackWidget />
12+
{element}
13+
</PageContext.Provider>
1014

1115
// Disable prefetching altogether so our bw is not destroyed.
1216
// If this turns out to hurt performance significantly, we can

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"@mdx-js/mdx": "^1.6.18",
1919
"@mdx-js/react": "^1.6.18",
2020
"@sentry-internal/global-search": "^0.5.7",
21-
"@sentry/browser": "7.55.2",
21+
"@sentry/browser": "7.69.0",
22+
"@sentry/core": "7.69.0",
2223
"@sentry/webpack-plugin": "2.2.2",
2324
"@types/dompurify": "^3.0.2",
2425
"@types/js-cookie": "^3.0.3",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import styled from '@emotion/styled';
3+
4+
const Button = styled.button`
5+
position: fixed;
6+
right: 0px;
7+
bottom: 50%;
8+
transform: translate(25%, 50%) rotate(-90deg);
9+
background-color: #fff;
10+
border: 1px solid #ccc;
11+
border-radius: 4px;
12+
color: #231c3d;
13+
cursor: pointer;
14+
font-size: 14px;
15+
font-weight: 600;
16+
padding: 6px 16px;
17+
text-align: center;
18+
text-decoration: none;
19+
z-index: 9000;
20+
&:hover {
21+
background-color: #eee;
22+
}
23+
&:focus-visible {
24+
outline: 1px solid #79628c;
25+
background-color: #eee;
26+
}
27+
`;
28+
29+
export function FeedbackButton(props) {
30+
return <Button {...props}>Feedback</Button>;
31+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import React, {FormEvent, useEffect, useRef} from 'react';
2+
import {css} from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
5+
import {useFocusTrap} from '../hooks/useFocusTrap';
6+
import {useShortcut} from '../hooks/useShortcut';
7+
8+
const Dialog = styled.dialog`
9+
background-color: rgba(0, 0, 0, 0.05);
10+
border: none;
11+
position: fixed;
12+
inset: 0;
13+
z-index: 10000;
14+
width: 100vw;
15+
height: 100vh;
16+
display: flex;
17+
align-items: center;
18+
justify-content: center;
19+
opacity: 1;
20+
transition: opacity 0.2s ease-in-out;
21+
&:not([open]) {
22+
opacity: 0;
23+
pointer-events: none;
24+
visibility: hidden;
25+
}
26+
`;
27+
28+
const Content = styled.div`
29+
border-radius: 4px;
30+
background-color: #fff;
31+
width: 500px;
32+
max-width: 100%;
33+
max-height: calc(100% - 64px);
34+
display: flex;
35+
flex-direction: column;
36+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 16px rgba(0, 0, 0, 0.2);
37+
transition: transform 0.2s ease-in-out;
38+
transform: translate(0, 0) scale(1);
39+
dialog:not([open]) & {
40+
transform: translate(0, -16px) scale(0.98);
41+
}
42+
`;
43+
44+
const Header = styled.h2`
45+
font-size: 20px;
46+
font-weight: 600;
47+
border-bottom: 1px solid #ccc;
48+
padding: 20px 24px;
49+
margin: 0px;
50+
`;
51+
52+
const Form = styled.form`
53+
display: flex;
54+
overflow: auto;
55+
flex-direction: column;
56+
gap: 16px;
57+
padding: 24px;
58+
`;
59+
60+
const Label = styled.label`
61+
display: flex;
62+
flex-direction: column;
63+
gap: 4px;
64+
margin: 0px;
65+
`;
66+
67+
const inputStyles = css`
68+
border: 1px solid #ccc;
69+
border-radius: 4px;
70+
font-size: 14px;
71+
padding: 6px 8px;
72+
&:focus {
73+
outline: 1px solid rgba(108, 95, 199, 1);
74+
border-color: rgba(108, 95, 199, 1);
75+
}
76+
`;
77+
78+
const Input = styled.input`
79+
${inputStyles}
80+
`;
81+
82+
const TextArea = styled.textarea`
83+
${inputStyles}
84+
min-height: 64px;
85+
resize: vertical;
86+
`;
87+
88+
const ModalFooter = styled.div`
89+
display: flex;
90+
justify-content: flex-end;
91+
gap: 8px;
92+
margin-top: 8px;
93+
`;
94+
95+
const buttonStyles = css`
96+
border: 1px solid #ccc;
97+
border-radius: 4px;
98+
cursor: pointer;
99+
font-size: 14px;
100+
font-weight: 600;
101+
padding: 6px 16px;
102+
`;
103+
104+
const SubmitButton = styled.button`
105+
${buttonStyles}
106+
background-color: rgba(108, 95, 199, 1);
107+
color: #fff;
108+
&:hover {
109+
background-color: rgba(88, 74, 192, 1);
110+
}
111+
&:focus-visible {
112+
outline: 1px solid rgba(108, 95, 199, 1);
113+
background-color: rgba(88, 74, 192, 1);
114+
}
115+
`;
116+
117+
const CancelButton = styled.button`
118+
${buttonStyles}
119+
background-color: #fff;
120+
color: #231c3d;
121+
font-weight: 500;
122+
&:hover {
123+
background-color: #eee;
124+
}
125+
&:focus-visible {
126+
outline: 1px solid rgba(108, 95, 199, 1);
127+
background-color: #eee;
128+
}
129+
`;
130+
131+
const FlexColumns = styled.div`
132+
display: flex;
133+
flex-direction: row;
134+
gap: 16px;
135+
& > * {
136+
flex: 1;
137+
}
138+
`;
139+
140+
interface FeedbackModalProps {
141+
onClose: () => void;
142+
onSubmit: (data: {comment: string; email: string; name: string}) => void;
143+
open: boolean;
144+
}
145+
146+
function stopPropagation(e: React.MouseEvent) {
147+
e.stopPropagation();
148+
}
149+
150+
const retrieveStringValue = (formData: FormData, key: string) => {
151+
const value = formData.get(key);
152+
if (typeof value === 'string') {
153+
return value.trim();
154+
}
155+
return '';
156+
};
157+
158+
export function FeedbackModal({open, onClose, onSubmit}: FeedbackModalProps) {
159+
const dialogRef = useRef<HTMLDialogElement>(null);
160+
const formRef = useRef<HTMLFormElement>(null);
161+
162+
useFocusTrap(dialogRef, open);
163+
useShortcut('Escape', onClose);
164+
165+
// Reset on close
166+
useEffect(() => {
167+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
168+
169+
if (!open) {
170+
timeoutId = setTimeout(() => {
171+
formRef.current.reset();
172+
}, 200);
173+
}
174+
return () => {
175+
if (timeoutId) {
176+
clearTimeout(timeoutId);
177+
}
178+
};
179+
}, [open]);
180+
181+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
182+
e.preventDefault();
183+
const formData = new FormData(e.target as HTMLFormElement);
184+
onSubmit({
185+
name: retrieveStringValue(formData, 'name'),
186+
email: retrieveStringValue(formData, 'email'),
187+
comment: retrieveStringValue(formData, 'comment'),
188+
});
189+
};
190+
191+
const user = window.Sentry?.getCurrentHub().getScope()?.getUser();
192+
193+
return (
194+
<Dialog id="feedbackModal" open={open} ref={dialogRef} onClick={onClose}>
195+
<Content onClick={stopPropagation}>
196+
<Header>Got any Feedback?</Header>
197+
<Form ref={formRef} onSubmit={handleSubmit}>
198+
<FlexColumns>
199+
<Label htmlFor="sentry-feedback-name">
200+
Your Name
201+
<Input
202+
type="text"
203+
id="sentry-feedback-name"
204+
name="name"
205+
placeholder="Anonymous"
206+
defaultValue={user?.username}
207+
/>
208+
</Label>
209+
<Label htmlFor="sentry-feedback-email">
210+
Your Email
211+
<Input
212+
type="text"
213+
id="sentry-feedback-email"
214+
name="email"
215+
placeholder="you@test.com"
216+
defaultValue={user?.email}
217+
/>
218+
</Label>
219+
</FlexColumns>
220+
<Label htmlFor="sentry-feedback-comment">
221+
Comment
222+
<TextArea
223+
onKeyDown={event => {
224+
if (event.key === 'Enter' && event.ctrlKey) {
225+
formRef.current.requestSubmit();
226+
}
227+
}}
228+
id="sentry-feedback-comment"
229+
name="comment"
230+
placeholder="Explain what bothers you"
231+
/>
232+
</Label>
233+
<ModalFooter>
234+
<CancelButton type="button" onClick={onClose}>
235+
Cancel
236+
</CancelButton>
237+
<SubmitButton type="submit">Submit</SubmitButton>
238+
</ModalFooter>
239+
</Form>
240+
</Content>
241+
</Dialog>
242+
);
243+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import {keyframes} from '@emotion/react';
3+
import styled from '@emotion/styled';
4+
5+
const Wrapper = styled.div`
6+
position: fixed;
7+
width: 100vw;
8+
padding: 8px;
9+
right: 0;
10+
bottom: 0;
11+
pointer-events: none;
12+
display: flex;
13+
justify-content: flex-end;
14+
transition: transform 0.4s ease-in-out;
15+
transform: translateY(0);
16+
z-index: 9000;
17+
&[data-hide='true'] {
18+
transform: translateY(120%);
19+
}
20+
`;
21+
22+
const borderColor = keyframes`
23+
0% {
24+
box-shadow: 0 2px 6px rgba(88, 74, 192, 1);
25+
border-color: rgba(88, 74, 192, 1);
26+
}
27+
20% {
28+
box-shadow: 0 2px 6px #FFC227;
29+
border-color: #FFC227;
30+
}
31+
40% {
32+
box-shadow: 0 2px 6px #FF7738;
33+
border-color: #FF7738;
34+
}
35+
60% {
36+
box-shadow: 0 2px 6px #33BF9E;
37+
border-color: #33BF9E;
38+
}
39+
80% {
40+
box-shadow: 0 2px 6px #F05781;
41+
border-color: #F05781;
42+
}
43+
100% {
44+
box-shadow: 0 2px 6px rgba(88, 74, 192, 1);
45+
border-color: rgba(88, 74, 192, 1);
46+
}
47+
`;
48+
49+
const Content = styled.div`
50+
background-color: #fff;
51+
border: 2px solid rgba(88, 74, 192, 1);
52+
border-radius: 20px;
53+
color: rgba(43, 34, 51, 1);
54+
font-size: 14px;
55+
padding: 6px 24px;
56+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 16px;
57+
box-shadow-color: red;
58+
animation: ${borderColor} 4s alternate infinite;
59+
`;
60+
61+
interface FeedbackSuccessMessageProps extends React.HTMLAttributes<HTMLDivElement> {
62+
show: boolean;
63+
}
64+
65+
export function FeedbackSuccessMessage({show, ...props}: FeedbackSuccessMessageProps) {
66+
return (
67+
<Wrapper data-hide={!show} {...props}>
68+
<Content>🎉 Thank you for your feedback! 🙌</Content>
69+
</Wrapper>
70+
);
71+
}

0 commit comments

Comments
 (0)