1+ import React , { useState } from 'react'
2+
3+ const FEEDBACK_TYPES = [ 'Bug' , 'Feature request' , 'Question' , 'Other' ] as const
4+
5+ export default function FeedbackForm ( ) {
6+ const [ email , setEmail ] = useState ( '' )
7+ const [ feedbackType , setFeedbackType ] = useState < ( typeof FEEDBACK_TYPES ) [ number ] | '' > ( '' )
8+ const [ followUp , setFollowUp ] = useState < boolean | null > ( null )
9+ const [ issue , setIssue ] = useState ( '' )
10+ const [ challenges , setChallenges ] = useState ( '' )
11+ const [ docsUsefulness , setDocsUsefulness ] = useState ( '' )
12+ const [ loading , setLoading ] = useState ( false )
13+ const [ status , setStatus ] = useState < 'idle' | 'success' | 'error' > ( 'idle' )
14+ const [ errorMsg , setErrorMsg ] = useState ( '' )
15+ const [ acceptedTerms , setAcceptedTerms ] = useState ( false )
16+
17+ async function onSubmit ( e : React . FormEvent ) {
18+ e . preventDefault ( )
19+ setLoading ( true )
20+ setStatus ( 'idle' )
21+ setErrorMsg ( '' )
22+
23+ try {
24+ const res = await fetch ( '/api/feedback' , {
25+ method : 'POST' ,
26+ headers : { 'Content-Type' : 'application/json' } ,
27+ body : JSON . stringify ( {
28+ email,
29+ feedbackType,
30+ issue,
31+ followUp,
32+ challenges,
33+ docsUsefulness,
34+ pageUrl : typeof window !== 'undefined' ? window . location . href : '' ,
35+ website : '' ,
36+ } ) ,
37+ } )
38+
39+ const data = await res . json ( ) . catch ( ( ) => ( { } ) )
40+ if ( ! res . ok || ! data . success ) {
41+ setStatus ( 'error' )
42+ setErrorMsg ( data ?. details || data ?. error || 'Failed to submit feedback.' )
43+ return
44+ }
45+
46+ setStatus ( 'success' )
47+ setEmail ( '' )
48+ setFeedbackType ( 'Bug' )
49+ setIssue ( '' )
50+ setFollowUp ( true )
51+ setChallenges ( '' )
52+ setDocsUsefulness ( '' )
53+ } catch {
54+ setStatus ( 'error' )
55+ setErrorMsg ( 'Network error. Please try again.' )
56+ } finally {
57+ setLoading ( false )
58+ }
59+ }
60+
61+ if ( status === 'success' ) {
62+ return (
63+ < div className = "rounded-large bg-light-surface-2 dark:bg-dark-surface-2 p-6 space-y-5" >
64+ < div className = "rounded-large overflow-hidden bg-light-pink-fade dark:bg-dark-pink-fade-80 mb-2" >
65+ < img
66+ src = "/img/feedback-banner.webp"
67+ alt = "Developers banner"
68+ className = "block w-full h-auto"
69+ loading = "lazy"
70+ />
71+ </ div >
72+
73+ < h2 className = "heading-3 text-light-neutral-1 dark:text-dark-neutral-1" >
74+ Thanks for submitting your feedback
75+ </ h2 >
76+ < p className = "body-2 text-light-neutral-2 dark:text-dark-neutral-2" >
77+ We appreciate it. Your input helps us improve Uniswap Docs.
78+ </ p >
79+
80+ < button
81+ type = "button"
82+ onClick = { ( ) => {
83+ setStatus ( 'idle' )
84+ setAcceptedTerms ( false )
85+ } }
86+ className = "rounded-large bg-light-pink-vibrant dark:bg-dark-pink-vibrant !text-white px-5 py-3 transition-colors hover:bg-light-pink-vibrant/70 dark:hover:bg-dark-pink-vibrant/70"
87+ >
88+ Submit another response
89+ </ button >
90+ </ div >
91+ )
92+ }
93+
94+ return (
95+ < form onSubmit = { onSubmit } className = "rounded-large bg-light-surface-2 dark:bg-dark-surface-2 p-6 space-y-5" >
96+
97+ < div className = "rounded-large overflow-hidden bg-light-pink-fade dark:bg-dark-pink-fade-80 mb-6" >
98+ < img
99+ src = "/img/feedback-banner.webp"
100+ alt = "Developers banner"
101+ className = "block w-full h-auto"
102+ loading = "lazy"
103+ />
104+ </ div >
105+
106+
107+ < div >
108+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" > Email*</ label >
109+ < input
110+ type = "email"
111+ required
112+ value = { email }
113+ onChange = { ( e ) => setEmail ( e . target . value ) }
114+ className = "mt-2 w-full rounded-large bg-light-surface-1 dark:bg-dark-surface-1 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
115+ />
116+ </ div >
117+
118+ < div >
119+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" > Type of feedback*</ label >
120+ < div className = "mt-2 space-y-2" >
121+ { FEEDBACK_TYPES . map ( ( type ) => (
122+ < label key = { type } className = "flex items-center gap-2 body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
123+ < input
124+ type = "radio"
125+ name = "feedback-type"
126+ value = { type }
127+ checked = { feedbackType === type }
128+ onChange = { ( ) => setFeedbackType ( type ) }
129+ required
130+ />
131+ { type }
132+ </ label >
133+ ) ) }
134+ </ div >
135+ </ div >
136+
137+ < div >
138+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" > What's the issue, idea, or question?*</ label >
139+ < textarea
140+ required
141+ value = { issue }
142+ onChange = { ( e ) => setIssue ( e . target . value ) }
143+ className = "mt-2 w-full min-h-[120px] rounded-large bg-light-surface-1 dark:bg-dark-surface-1 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
144+ />
145+ </ div >
146+
147+ < div >
148+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" > Can we follow up with you about your feedback?*</ label >
149+ < div className = "mt-2 flex gap-4" >
150+ < label className = "flex items-center gap-2 body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
151+ < input
152+ type = "radio"
153+ name = "follow-up"
154+ checked = { followUp === true }
155+ onChange = { ( ) => setFollowUp ( true ) }
156+ required
157+ />
158+ Yes
159+ </ label >
160+ < label className = "flex items-center gap-2 body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
161+ < input
162+ type = "radio"
163+ name = "follow-up"
164+ checked = { followUp === false }
165+ onChange = { ( ) => setFollowUp ( false ) }
166+ required
167+ />
168+ No
169+ </ label >
170+ </ div >
171+ </ div >
172+
173+ < div >
174+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
175+ What has been the most challenging part of building on or integrating with Uniswap? (Optional)
176+ </ label >
177+ < textarea
178+ value = { challenges }
179+ onChange = { ( e ) => setChallenges ( e . target . value ) }
180+ className = "mt-2 w-full min-h-[100px] rounded-large bg-light-surface-1 dark:bg-dark-surface-1 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
181+ />
182+ </ div >
183+
184+ < div >
185+ < label className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
186+ Have you found Uniswap docs to be useful? (Optional)
187+ </ label >
188+ < textarea
189+ value = { docsUsefulness }
190+ onChange = { ( e ) => setDocsUsefulness ( e . target . value ) }
191+ className = "mt-2 w-full min-h-[100px] rounded-large bg-light-surface-1 dark:bg-dark-surface-1 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
192+ placeholder = "What sections have been most helpful? Are there any that felt confusing, incomplete, or missing?"
193+ />
194+ </ div >
195+
196+ < input type = "text" name = "website" className = "hidden" tabIndex = { - 1 } autoComplete = "off" />
197+
198+
199+
200+ < label className = "flex items-start gap-2 body-2 text-light-neutral-2 dark:text-dark-neutral-2" >
201+ < input
202+ type = "checkbox"
203+ checked = { acceptedTerms }
204+ onChange = { ( e ) => setAcceptedTerms ( e . target . checked ) }
205+ className = "mt-1"
206+ required
207+ />
208+ < span >
209+ By submitting, I agree to{ ' ' }
210+ < a
211+ className = "body-2 text-light-accent-1 dark:text-dark-accent-1 underline"
212+ href = "https://support.uniswap.org/hc/en-us/articles/30935100859661-Uniswap-Labs-Terms-of-Service"
213+ target = "_blank"
214+ rel = "noreferrer"
215+ >
216+ Uniswap Labs Terms of Service
217+ </ a > { ' ' }
218+ and{ ' ' }
219+ < a
220+ className = "body-2 text-light-accent-1 dark:text-dark-accent-1 underline"
221+ href = "https://support.uniswap.org/hc/en-us/articles/30934457771405-Uniswap-Labs-Privacy-Policy"
222+ target = "_blank"
223+ rel = "noreferrer"
224+ >
225+ Privacy Policy
226+ </ a >
227+ .
228+ </ span >
229+ </ label >
230+
231+ < button
232+ type = "submit"
233+ disabled = { loading || ! acceptedTerms }
234+ className = "rounded-large bg-light-pink-vibrant dark:bg-dark-pink-vibrant !text-white px-5 py-3 transition-colors hover:bg-light-pink-vibrant/70 dark:hover:bg-dark-pink-vibrant/70 active:bg-light-pink-vibrant/70 active:dark:bg-dark-pink-vibrant/70 active:!text-white disabled:!text-white disabled:opacity-60"
235+ >
236+ { loading ? 'Submitting...' : 'Submit' }
237+ </ button >
238+
239+ { status === 'success' && (
240+ < div className = "space-y-2" >
241+ < p className = "body-2 text-light-neutral-1 dark:text-dark-neutral-1" >
242+ Thank you for your feedback!
243+ </ p >
244+ < p className = "body-2 text-light-neutral-2 dark:text-dark-neutral-2" >
245+ We really appreciate you taking the time to share your thoughts.
246+ </ p >
247+ < p className = "body-2 text-light-neutral-2 dark:text-dark-neutral-2" >
248+ If you left your contact info, we’ll follow up with updates or questions as we make improvements.
249+ </ p >
250+ </ div >
251+ ) }
252+ { status === 'error' && < p className = "body-2 text-light-orange-vibrant" > { errorMsg } </ p > }
253+
254+ </ form >
255+ )
256+ }
0 commit comments