Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 46 additions & 12 deletions api/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,68 @@ const HUBSPOT_PORTAL_ID = '47435488'
const HUBSPOT_FORM_ID = '80c7a6ab-9b96-412f-9469-aa2bc14faa18'
const HUBSPOT_SUBMIT_URL = `https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_FORM_ID}`

const FEEDBACK_TYPES = new Set(['Bug', 'Feature request', 'Question', 'Other'])
const MAX_EMAIL_LENGTH = 254
const MAX_ISSUE_LENGTH = 2000
const MAX_CHALLENGES_LENGTH = 1000
const MAX_DOCS_USEFULNESS_LENGTH = 1000
const MAX_PAGE_URL_LENGTH = 2048

const DISALLOWED_CHARS_REGEX = /[<>&"']/g

function isValidEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}

function sanitizeText(value: unknown, maxLength: number): string {
if (typeof value !== 'string') return ''
return value.replace(DISALLOWED_CHARS_REGEX, '').trim().slice(0, maxLength)
}

export default async function handler(req: any, res: any) {
if (req.method !== 'POST') {
return res.status(405).json({ success: false, error: 'Method not allowed' })
}


try {
const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body || {}
const { email, feedbackType, issue, followUp, challenges, docsUsefulness, pageUrl, website } = body

const website = sanitizeText(body.website, 200)
if (website) return res.status(200).json({ success: true }) // honeypot
if (!email || !isValidEmail(email)) return res.status(400).json({ success: false, error: 'Invalid email' })
if (!feedbackType || !issue) return res.status(400).json({ success: false, error: 'Missing required fields' })

const email = sanitizeText(body.email, MAX_EMAIL_LENGTH)
const feedbackType = sanitizeText(body.feedbackType, 50)
const issue = sanitizeText(body.issue, MAX_ISSUE_LENGTH)
const challenges = sanitizeText(body.challenges, MAX_CHALLENGES_LENGTH)
const docsUsefulness = sanitizeText(body.docsUsefulness, MAX_DOCS_USEFULNESS_LENGTH)
const pageUrl = sanitizeText(body.pageUrl, MAX_PAGE_URL_LENGTH)
const followUp = body.followUp


if (!email || !isValidEmail(email)) {
return res.status(400).json({ success: false, error: 'Invalid email' })
}

if (!FEEDBACK_TYPES.has(feedbackType)) {
return res.status(400).json({ success: false, error: 'Invalid feedback type' })
}

if (!issue) {
return res.status(400).json({ success: false, error: 'Missing required fields' })
}

if (typeof followUp !== 'boolean') {
return res.status(400).json({ success: false, error: 'Invalid follow-up value' })
}

const fields = [
{ name: 'email', value: String(email) },
{ name: 'type_of_feedback', value: String(feedbackType) },
{ name: 'whats_the_issue_idea_or_question', value: String(issue) },
{ name: 'email', value: email },
{ name: 'type_of_feedback', value: feedbackType },
{ name: 'whats_the_issue_idea_or_question', value: issue },
{ name: 'can_we_follow_up_with_you_about_your_feedback', value: followUp ? 'Yes' : 'No' },
...(challenges?.trim()
? [{ name: 'what_has_been_the_most_challenging_part_of_building_on_or_integrating_with_uniswap', value: challenges.trim() }]
: []),
...(docsUsefulness?.trim()
? [{ name: 'have_you_found_uniswap_docs_to_be_useful', value: docsUsefulness.trim() }]
: []),
...(challenges ? [{ name: 'what_has_been_the_most_challenging_part_of_building_on_or_integrating_with_uniswap', value: challenges }] : []),
...(docsUsefulness ? [{ name: 'have_you_found_uniswap_docs_to_be_useful', value: docsUsefulness }] : []),
]

const payload = {
Expand Down
10 changes: 5 additions & 5 deletions docs/api/trading/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ The Uniswap Trading API provides quote generation and transaction building for t

## Get your API key

Create an account in the [Trading API Developer Portal](https://developers.uniswap.org/dashboard/) to generate your API key.
Create an account in the [Trading API Developer Platform](https://developers.uniswap.org/dashboard/) to generate your API key.

:::info
For complete endpoint coverage, authentication requirements, and implementation patterns, see the [API Integration Guide](https://api-docs.uniswap.org/guides/integration_guide).
:::

## Quick Start

Get started with the Uniswap Agent CLI skill, or send a direct request using the cURL example below.
Choose the setup that fits your workflow: AI-assisted integration with Uniswap Skills, or direct API calls via cURL.

### Agent CLI
### AI Setup (Uniswap Skills)

If you are using Agent CLI:
Install the Uniswap AI skills package with the swap integration skill:

```bash
npx skills add uniswap/uniswap-ai --skill swap-integration
```

### cURL Reference
### Direct API Request (cURL)

> Never commit real API keys to the repository. Use environment variables or placeholders.

Expand Down
58 changes: 29 additions & 29 deletions src/components/FeedbackForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import React, { useState } from 'react'

const FEEDBACK_TYPES = ['Bug', 'Feature request', 'Question', 'Other'] as const

const DISALLOWED_CHARS_REGEX = /[<>&"']/g

function sanitizeInput(value: string, maxLength: number): string {
return value.replace(DISALLOWED_CHARS_REGEX, '').slice(0, maxLength)
}

export default function FeedbackForm() {
const [email, setEmail] = useState('')
const [feedbackType, setFeedbackType] = useState<(typeof FEEDBACK_TYPES)[number] | ''>('')
Expand Down Expand Up @@ -45,9 +51,9 @@ export default function FeedbackForm() {

setStatus('success')
setEmail('')
setFeedbackType('Bug')
setFeedbackType('')
setIssue('')
setFollowUp(true)
setFollowUp(null)
setChallenges('')
setDocsUsefulness('')
} catch {
Expand All @@ -60,13 +66,16 @@ export default function FeedbackForm() {

if (status === 'success') {
return (
<div className="p-0 space-y-8">
<div className="p-0 space-y-5">

<h2 className="heading-3 text-light-neutral-1 dark:text-dark-neutral-1">
Thanks for submitting your feedback
</h2>
<p className="body-2 text-light-neutral-2 dark:text-dark-neutral-2">
We appreciate it. Your input helps us improve Uniswap Docs.
We really appreciate you taking the time to share your thoughts.
</p>
<p className="body-2 text-light-neutral-2 dark:text-dark-neutral-2">
If you left your contact info, we'll follow up with updates or questions as we make improvements.
</p>

<button
Expand All @@ -84,7 +93,7 @@ export default function FeedbackForm() {
}

return (
<form onSubmit={onSubmit} className="space-y-8">
<form onSubmit={onSubmit} className="space-y-5">


<div>
Expand All @@ -95,7 +104,8 @@ export default function FeedbackForm() {
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
onChange={(e) => setEmail(sanitizeInput(e.target.value, 254))}
maxLength={254}
className="mt-2 w-full rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
/>
</div>
Expand Down Expand Up @@ -125,12 +135,13 @@ export default function FeedbackForm() {
<label className="body-2 text-light-neutral-1 dark:text-dark-neutral-1">
What's the issue, idea, or question?<span className="text-light-pink-vibrant dark:text-dark-pink-vibrant ml-1 font-normal">*</span>
</label>
<textarea
required
value={issue}
onChange={(e) => setIssue(e.target.value)}
className="mt-2 w-full rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
/>
<textarea
required
value={issue}
onChange={(e) => setIssue(sanitizeInput(e.target.value, 2000))}
maxLength={2000}
className="mt-2 w-full resize-none rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
/>
</div>

<div>
Expand Down Expand Up @@ -167,8 +178,9 @@ export default function FeedbackForm() {
</label>
<textarea
value={challenges}
onChange={(e) => setChallenges(e.target.value)}
className="mt-2 w-full rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
onChange={(e) => setChallenges(sanitizeInput(e.target.value, 1000))}
maxLength={1000}
className="mt-2 w-full resize-none rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
/>
</div>

Expand All @@ -178,8 +190,9 @@ export default function FeedbackForm() {
</label>
<textarea
value={docsUsefulness}
onChange={(e) => setDocsUsefulness(e.target.value)}
className="mt-2 w-full rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
onChange={(e) => setDocsUsefulness(sanitizeInput(e.target.value, 1000))}
maxLength={1000}
className="mt-2 w-full resize-none rounded-large bg-light-surface-2 dark:bg-dark-surface-2 border border-light-surface-3 dark:border-dark-surface-3 p-3 text-light-neutral-1 dark:text-dark-neutral-1"
placeholder="What sections have been most helpful? Are there any that felt confusing, incomplete, or missing?"
/>
</div>
Expand Down Expand Up @@ -227,19 +240,6 @@ export default function FeedbackForm() {
{loading ? 'Submitting...' : 'Submit'}
</button>

{status === 'success' && (
<div className="space-y-2">
<p className="body-2 text-light-neutral-1 dark:text-dark-neutral-1">
Thank you for your feedback!
</p>
<p className="body-2 text-light-neutral-2 dark:text-dark-neutral-2">
We really appreciate you taking the time to share your thoughts.
</p>
<p className="body-2 text-light-neutral-2 dark:text-dark-neutral-2">
If you left your contact info, we’ll follow up with updates or questions as we make improvements.
</p>
</div>
)}
{status === 'error' && <p className="body-2 text-light-orange-vibrant">{errorMsg}</p>}

</form>
Expand Down
57 changes: 52 additions & 5 deletions src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ html:has(.theme-doc-markdown) {
}

.DocSearch-Button-Container {
@apply sm:w-64 font-normal;
@apply font-normal;
}

.DocSearch-Search-Icon {
Expand Down Expand Up @@ -221,15 +221,40 @@ html:has(.theme-doc-markdown) {
.theme-doc-footer-edit-meta-row {
@apply !hidden;
}

.navbar {
@apply !p-0;
box-shadow: none !important;
border-bottom: none !important;
background-color: theme(colors.light-surface-1) !important;
opacity: 1 !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}

.navbar .navbar__inner {
background-color: theme(colors.light-surface-1) !important;
box-shadow: inset 0 -1px 0 theme(colors.light-surface-3);
}

:root[data-theme='dark'] .navbar {
background-color: theme(colors.dark-surface-1) !important;
}

:root[data-theme='dark'] .navbar .navbar__inner {
background-color: theme(colors.dark-surface-1) !important;
box-shadow: none !important;
}

.navbar-sidebar {
transform: translate3d(100%, 0, 0);
left: auto;
right: 0;
}

.navbar-sidebar--show .navbar-sidebar {
transform: translate3d(0, 0, 0) !important;
}

.theme-doc-markdown {
@apply !body-2 flex flex-col space-y-6;
}
Expand Down Expand Up @@ -311,8 +336,19 @@ html:has(.theme-doc-markdown) {
font-family: system-ui;
}

.navbar .DocSearch {
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
box-sizing: border-box;
margin: 0 !important;
}

.navbar .DocSearch-Button {
width: 172px;
width: 100% !important;
min-width: 0 !important;
max-width: 100% !important;
box-sizing: border-box;
height: 32px;
display: flex;
align-items: center;
Expand Down Expand Up @@ -364,7 +400,17 @@ html:has(.theme-doc-markdown) {
color: theme(colors.dark-neutral-2);
}

@media (max-width: 639px) {
@media (min-width: 768px) and (max-width: 1099px) {
.navbar .DocSearch-Button {
padding: 0 10px;
}

.navbar .DocSearch-Button-Key {
display: none;
}
}

@media (max-width: 767px) {
.navbar .DocSearch-Button {
width: 100% !important;
min-width: 0 !important;
Expand All @@ -375,8 +421,9 @@ html:has(.theme-doc-markdown) {
}

.navbar .DocSearch-Button-Container {
width: 100% !important;
width: 100%;
min-width: 0 !important;
font-weight: 400;
}

.navbar .DocSearch-Button-Key {
Expand Down
Loading
Loading