Skip to content

Commit 5d4ad0f

Browse files
Temp feedback form component
1 parent 2af616c commit 5d4ad0f

7 files changed

Lines changed: 345 additions & 4 deletions

File tree

api/feedback.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const HUBSPOT_PORTAL_ID = '47435488'
2+
const HUBSPOT_FORM_ID = '80c7a6ab-9b96-412f-9469-aa2bc14faa18'
3+
const HUBSPOT_SUBMIT_URL = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}`
4+
5+
function isValidEmail(value: string): boolean {
6+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
7+
}
8+
9+
export default async function handler(req: any, res: any) {
10+
if (req.method !== 'POST') {
11+
return res.status(405).json({ success: false, error: 'Method not allowed' })
12+
}
13+
14+
try {
15+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body || {}
16+
const { email, feedbackType, issue, followUp, challenges, docsUsefulness, pageUrl, website } = body
17+
18+
if (website) return res.status(200).json({ success: true }) // honeypot
19+
if (!email || !isValidEmail(email)) return res.status(400).json({ success: false, error: 'Invalid email' })
20+
if (!feedbackType || !issue) return res.status(400).json({ success: false, error: 'Missing required fields' })
21+
22+
const fields = [
23+
{ name: 'email', value: String(email) },
24+
{ name: 'type_of_feedback', value: String(feedbackType) },
25+
{ name: 'whats_the_issue_idea_or_question', value: String(issue) },
26+
{ name: 'can_we_follow_up_with_you_about_your_feedback', value: followUp ? 'Yes' : 'No' },
27+
...(challenges?.trim()
28+
? [{ name: 'what_has_been_the_most_challenging_part_of_building_on_or_integrating_with_uniswap', value: challenges.trim() }]
29+
: []),
30+
...(docsUsefulness?.trim()
31+
? [{ name: 'have_you_found_uniswap_docs_to_be_useful', value: docsUsefulness.trim() }]
32+
: []),
33+
]
34+
35+
const payload = {
36+
fields,
37+
legalConsentOptions: {
38+
consent: {
39+
consentToProcess: true,
40+
text: 'By submitting, I agree to Uniswap Labs Terms of Service and Privacy Policy.',
41+
},
42+
},
43+
context: {
44+
pageUri: pageUrl || '',
45+
pageName: 'Feedback | Uniswap Docs',
46+
},
47+
}
48+
49+
const hsRes = await fetch(HUBSPOT_SUBMIT_URL, {
50+
method: 'POST',
51+
headers: { 'Content-Type': 'application/json' },
52+
body: JSON.stringify(payload),
53+
})
54+
55+
if (!hsRes.ok) {
56+
const hsText = await hsRes.text()
57+
return res.status(502).json({ success: false, error: 'HubSpot submission failed', details: hsText })
58+
}
59+
60+
return res.status(200).json({ success: true })
61+
} catch {
62+
return res.status(500).json({ success: false, error: 'Internal server error' })
63+
}
64+
}

src/components/FeedbackForm.tsx

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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+
}

src/css/custom.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,14 @@ html:has(.theme-doc-markdown) {
311311
font-family: system-ui;
312312
}
313313

314+
/* Feedback page shell for HubSpot embed */
315+
.docs-feedback-shell {
316+
border: 1px solid theme(colors.light-surface-3);
317+
}
318+
:root[data-theme='dark'] .docs-feedback-shell {
319+
border-color: theme(colors.dark-surface-3);
320+
}
321+
314322
/**
315323
* Copyright (c) Facebook, Inc. and its affiliates.
316324
*

src/pages/feedback.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react'
2+
import Layout from '@theme/Layout'
3+
import FeedbackForm from '@site/src/components/FeedbackForm'
4+
5+
export default function FeedbackPage() {
6+
return (
7+
<Layout title="Submit Feedback" description="Share your feedback with Uniswap Labs">
8+
<main className="content-page-padding py-padding-x-large">
9+
<h1 className="heading-2 text-light-neutral-1 dark:text-dark-neutral-1 mb-6">Submit Feedback</h1>
10+
<FeedbackForm />
11+
</main>
12+
</Layout>
13+
)
14+
}

src/theme/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const footerData = {
1515
},
1616
{
1717
label: 'Feedback',
18-
href: 'https://share.hsforms.com/1gMemq5uWQS-UaaorwU-qGAs8pgg',
18+
href: '/feedback',
1919
},
2020
{
2121
label: 'Bug Bounty',

src/theme/Navbar/Content/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ export default function NavbarContent(): ReactNode {
8686
<NavbarColorModeToggle />
8787
<Link
8888
className="button-label-4 py-2 px-3 bg-light-accent-2 dark:bg-dark-accent-2 hover:bg-light-accent-2-hovered hover:dark:bg-dark-accent-2-hovered transition rounded-small"
89-
to="https://share.hsforms.com/1gMemq5uWQS-UaaorwU-qGAs8pgg"
90-
target="_blank"
91-
rel="noreferrer"
89+
to="/feedback"
90+
target="_self"
9291
>
9392
<span className="text-light-accent-1 dark:text-dark-accent-1">Submit Feedback</span>
9493
</Link>

static/img/feedback-banner.webp

140 KB
Loading

0 commit comments

Comments
 (0)