Feedback Button in React
Prerequisites
Section titled “Prerequisites”- Nordva Launch account with a publishable API key (
nv_pub_...) - React 17+ or Next.js
Option A: Script-tag widget (zero code)
Section titled “Option A: Script-tag widget (zero code)”Drop this before </body> in your HTML:
<script src="https://cdn.nordva.dev/v1/feedback.js" data-key="nv_pub_live_YOUR_KEY" data-position="bottom-right" data-accent="#F59E0B" data-user-id="OPTIONAL_USER_ID" data-user-email="OPTIONAL_USER_EMAIL"></script>A floating “Feedback” button appears in the corner. Clicking it opens a drop-up modal. Submitted feedback is auto-classified by Claude Haiku and routed to your configured destinations (email, Slack, Linear).
Attributes:
| Attribute | Default | Description |
|---|---|---|
data-key | required | Your publishable key (nv_pub_...) |
data-position | bottom-right | bottom-right or bottom-left |
data-accent | #F59E0B | Button and submit color (hex) |
data-user-id | — | Your app’s user identifier — stored for filtering |
data-user-email | — | The signed-in user’s email, so you can reply to their feedback |
data-ask-email | false | Set to true to add an optional “Email (if you want a reply)” field. Ignored when data-user-email is set |
The email is optional in every form. When one is given it is returned as email on GET /v1/feedback, shown under Reply to in the dashboard and included in the MCP list_feedback results. It is not forwarded to your routing destinations, so it never lands in a public GitHub issue.
Triage
Section titled “Triage”Every submission has a status. Launch sets received, classified, routed, partial_routed or failed_classification; you set reviewed or resolved:
curl -X PATCH https://api.nordva.dev/v1/feedback/fb_01J... \ -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "resolved" }'Send classification in the same call to correct the classifier. A correction gets confidence 1 and is not routed again, so it never creates a second issue. GET /v1/feedback?status=open lists everything not yet reviewed or resolved. From an MCP client, ask your assistant to mark an item resolved; it uses update_feedback.
Option B: Custom React component
Section titled “Option B: Custom React component”For full UI control:
'use client';
import { useState } from 'react';
interface FeedbackButtonProps { publishableKey: string; userId?: string; /** Optional. Lets you reply to the person who sent the feedback. */ userEmail?: string;}
export function FeedbackButton({ publishableKey, userId, userEmail }: FeedbackButtonProps) { const [open, setOpen] = useState(false); const [message, setMessage] = useState(''); const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle');
async function submit() { if (message.trim().length < 5) return; setStatus('loading');
const res = await fetch('https://api.nordva.dev/v1/feedback', { method: 'POST', headers: { 'Authorization': `Bearer ${publishableKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ feedback_text: message.trim(), user_id: userId, email: userEmail, page_url: window.location.href, }), });
if (res.ok) { setStatus('done'); setTimeout(() => { setOpen(false); setStatus('idle'); setMessage(''); }, 2000); } else { setStatus('error'); } }
return ( <> <button onClick={() => setOpen(o => !o)} style={{ position: 'fixed', bottom: 20, right: 20 }} > Feedback </button>
{open && ( <div style={{ position: 'fixed', bottom: 72, right: 20, width: 280, background: '#fff', borderRadius: 12, boxShadow: '0 8px 32px rgba(0,0,0,0.18)', padding: 20 }}> <button onClick={() => setOpen(false)} style={{ position: 'absolute', top: 10, right: 14, background: 'none', border: 'none', cursor: 'pointer', fontSize: 18 }}>×</button> <h3 style={{ margin: '0 0 12px', fontSize: 14 }}>Send feedback</h3>
{status === 'done' ? ( <p style={{ color: '#10b981', fontSize: 13 }}>Thanks — feedback received.</p> ) : ( <> <textarea value={message} onChange={e => setMessage(e.target.value)} placeholder="Tell us what you think..." style={{ width: '100%', boxSizing: 'border-box', minHeight: 80, padding: 10, fontSize: 13, borderRadius: 6, border: '1px solid #e5e7eb', resize: 'vertical' }} /> <button onClick={submit} disabled={status === 'loading'} style={{ marginTop: 10, width: '100%', padding: 9, background: '#F59E0B', color: '#000', border: 'none', borderRadius: 6, fontWeight: 600, cursor: 'pointer' }} > {status === 'loading' ? 'Sending...' : 'Send'} </button> {status === 'error' && <p style={{ color: '#ef4444', fontSize: 12, marginTop: 8 }}>Failed — please try again.</p>} </> )} </div> )} </> );}Use it in your layout:
import { FeedbackButton } from '@/components/FeedbackButton';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> {children} <FeedbackButton publishableKey={process.env.NEXT_PUBLIC_NORDVA_KEY!} userId="user_abc123" /> </body> </html> );}View and triage feedback
Section titled “View and triage feedback”In the Nordva Launch dashboard → Feedback, or via the API:
curl "https://api.nordva.dev/v1/feedback?category=bug&status=open" \ -H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"Route feedback to Slack or Linear
Section titled “Route feedback to Slack or Linear”In the dashboard → Feedback → Routing, connect a destination. All classified feedback matching your filter rules gets forwarded automatically.