Conversation
- Add "beta" label next to the Certified wordmark in navbar - Add floating "Share Feedback" button (bottom-right) that opens a modal - Feedback modal includes message textarea and optional email field with validation - API route sends feedback to support@hypercerts.org via Resend - Sends confirmation email to user if they provide an email address - Feedback button stays above footer when scrolled to bottom - Modal has expand/shrink toggle on desktop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add beta label and feedback modal
- Add "beta" label next to the Certified wordmark in navbar - Add floating "Share Feedback" button (bottom-right) that opens a modal - Feedback modal includes message textarea and optional email field with validation - API route sends feedback to support@hypercerts.org via Resend - Sends confirmation email to user if they provide an email address - Feedback button stays above footer when scrolled to bottom - Modal has expand/shrink toggle on desktop Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hide trigger button when modal is open - Add visual feedback when swiping up to expand - Persist form content when closing and reopening - Increase handle touch target size Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds a client-side feedback modal (desktop overlay + mobile bottom-sheet), a POST API endpoint that sends emails via Resend, example env vars and dependency for Resend, CSS for modal/UI, a navbar beta label, and privacy text updates disclosing Vercel analytics. Changes
sequenceDiagram
actor User
participant Client as Feedback Modal (Client)
participant Server as /api/feedback (Server)
participant Resend as Resend (Email Service)
User->>Client: Open modal, enter message (and optional email)
User->>Client: Submit form
Client->>Client: Validate message and email
alt Validation fails
Client->>User: Show inline error
else Validation passes
Client->>Server: POST /api/feedback {message, email?}
Server->>Server: Check origin/host and validate payload
alt Invalid request
Server-->>Client: 400/403 response
Client->>User: Show error
else Valid
Server->>Resend: Send support email to support@hypercerts.org
Resend-->>Server: Acknowledgement
alt email provided
Server->>Resend: Send confirmation email to user
Resend-->>Server: Acknowledgement
end
Server-->>Client: { success: true }
Client->>User: Show confirmation view
end
end
🎯 4 (Complex) | ⏱️ ~50 minutes
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (2)
src/app/api/feedback/route.ts (1)
36-54: Consider graceful handling if confirmation email fails.If the support email succeeds (line 21-33) but the user confirmation email fails, the entire request returns 500. The user's feedback was received, but they get an error. Consider handling the confirmation email failure gracefully.
Proposed approach
// Send confirmation to user if they provided an email if (email) { - await resend.emails.send({ + try { + await resend.emails.send({ - from: FROM_EMAIL, - to: email, - subject: "Thank you for your feedback to Certified.app", - text: [ - "Thank you for sharing your feedback with us!", - "", - "We've received the following message:", - "", - `"${message}"`, - "", - "We appreciate your input and will review it carefully.", - "", - "Best regards,", - "The Certified Team", - ].join("\n"), - }); + from: FROM_EMAIL, + to: email, + subject: "Thank you for your feedback to Certified.app", + text: [ + "Thank you for sharing your feedback with us!", + "", + "We've received the following message:", + "", + `"${message}"`, + "", + "We appreciate your input and will review it carefully.", + "", + "Best regards,", + "The Certified Team", + ].join("\n"), + }); + } catch (confirmErr) { + console.error("Failed to send confirmation email:", confirmErr); + // Don't fail the request - feedback was received + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/feedback/route.ts` around lines 36 - 54, Wrap the user-confirmation email send (the resend.emails.send call that uses FROM_EMAIL and to: email) in a try/catch so failure to send the confirmation does not bubble up and turn the whole request into a 500; after catching, log the error (use existing logger or console.error) with context like "confirmation email failed" and continue returning the normal success response (the same response you return when the support email succeeds).src/components/ui/feedback-modal.tsx (1)
66-73: Consider returning focus to trigger when modal closes via Escape.The Escape key handler closes the modal but doesn't return focus to the trigger button. For better keyboard accessibility, focus should be restored to the element that opened the modal.
Proposed approach
+ const triggerRef = useRef<HTMLButtonElement>(null) + useEffect(() => { if (!isOpen) return const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") setIsOpen(false) + if (e.key === "Escape") { + setIsOpen(false) + setTimeout(() => triggerRef.current?.focus(), 0) + } } document.addEventListener("keydown", handleKeyDown) return () => document.removeEventListener("keydown", handleKeyDown) }, [isOpen])And add
ref={triggerRef}to the trigger button.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/feedback-modal.tsx` around lines 66 - 73, The Escape handler in the useEffect closes the modal but doesn't restore focus; store a ref to the trigger element (e.g., add triggerRef and attach it to the trigger button) and when handling Escape/closing (in handleKeyDown or wherever setIsOpen(false) is invoked) call triggerRef.current?.focus() after closing; update the useEffect/handleKeyDown logic in the feedback-modal component (useEffect, handleKeyDown, setIsOpen) to capture and restore focus so keyboard users return to the trigger when the modal closes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.env.local.example:
- Line 24: Update the example RESEND_FROM_EMAIL in .env.local.example to match
the actual default used in the code (change noreply@certified.one to
no-reply@certified.one) so developers aren't confused; ensure the
RESEND_FROM_EMAIL example value matches the default email string used in
route.ts (the code path that falls back to the env var/default).
In `@src/app/api/feedback/route.ts`:
- Around line 8-14: Add a CSRF Origin check at the start of the POST handler
(function POST) by reading the request Origin header (req.headers.get("origin"))
and validating it against the allowed origin(s) or the app's expected host; if
the origin is missing or not allowed, return NextResponse.json({ error: "Invalid
origin" }, { status: 403 }) before processing the payload. Ensure this
validation happens before parsing req.json() and before any other logic so the
handler rejects cross-site requests early; update any config/constant used for
allowed origins and reference it from the POST function for maintainability.
- Line 4: Resend client is initialized unconditionally which hides missing API
key errors; before constructing the Resend instance (the const resend = new
Resend(...) line) validate process.env.RESEND_API_KEY and fail fast with a clear
message or throw an error (or alternatively check inside the request handler and
return a helpful 4xx error) so that the Resend class is not instantiated with an
undefined key; update the code around the Resend initialization (referencing the
Resend symbol and RESEND_API_KEY env var) to validate and handle the missing key
explicitly.
In `@src/app/globals.css`:
- Around line 183-194: The .navbar__beta-label rule violates styling guidelines:
remove or neutralize the uppercase transformation by deleting text-transform:
uppercase (or set text-transform: none) in the .navbar__beta-label CSS, and
replace the hard-coded border-radius: 4px with border-radius: var(--radius) so
the component uses the shared radius variable.
In `@src/components/ui/feedback-modal.tsx`:
- Around line 240-242: The error paragraphs rendering emailError and error
currently lack ARIA roles; update the two JSX elements that render emailError
and error (the ones using the variables emailError and error and className
"feedback-modal__error") to include role="alert" so screen readers announce them
immediately; ensure both conditional renders include the attribute (e.g., <p
role="alert" ...>) without changing surrounding markup or styling.
- Around line 302-321: The mobile bottom sheet created via createPortal is
missing ARIA dialog semantics, so add appropriate accessibility attributes to
the bottom-sheet container: set role="dialog", aria-modal="true" and an
accessible label/ description (aria-labelledby or aria-label) on the div with
className `bottom-sheet feedback-bottom-sheet` (the element referenced by
sheetRef), and ensure focus management moves focus into that element when opened
and returns focus on close; also mirror any existing desktop modal IDs used for
labels to reuse the same label element for consistency.
---
Nitpick comments:
In `@src/app/api/feedback/route.ts`:
- Around line 36-54: Wrap the user-confirmation email send (the
resend.emails.send call that uses FROM_EMAIL and to: email) in a try/catch so
failure to send the confirmation does not bubble up and turn the whole request
into a 500; after catching, log the error (use existing logger or console.error)
with context like "confirmation email failed" and continue returning the normal
success response (the same response you return when the support email succeeds).
In `@src/components/ui/feedback-modal.tsx`:
- Around line 66-73: The Escape handler in the useEffect closes the modal but
doesn't restore focus; store a ref to the trigger element (e.g., add triggerRef
and attach it to the trigger button) and when handling Escape/closing (in
handleKeyDown or wherever setIsOpen(false) is invoked) call
triggerRef.current?.focus() after closing; update the useEffect/handleKeyDown
logic in the feedback-modal component (useEffect, handleKeyDown, setIsOpen) to
capture and restore focus so keyboard users return to the trigger when the modal
closes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a8fcd550-8fd0-4f74-866d-6c52e07c4aa3
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
.env.local.examplepackage.jsonsrc/app/api/feedback/route.tssrc/app/globals.csssrc/app/layout.tsxsrc/app/privacy/page.tsxsrc/components/layout/navbar.tsxsrc/components/ui/feedback-modal.tsx
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { Resend } from "resend"; | ||
|
|
||
| const resend = new Resend(process.env.RESEND_API_KEY); |
There was a problem hiding this comment.
Add validation for missing API key.
If RESEND_API_KEY is not set, the Resend client initialization succeeds but email sending will fail at runtime with an unclear error. Consider failing fast with a clear message.
Proposed fix
+if (!process.env.RESEND_API_KEY) {
+ console.warn("RESEND_API_KEY is not set - feedback emails will fail");
+}
+
const resend = new Resend(process.env.RESEND_API_KEY);Or alternatively, return a more helpful error in the handler if the key is missing.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const resend = new Resend(process.env.RESEND_API_KEY); | |
| if (!process.env.RESEND_API_KEY) { | |
| console.warn("RESEND_API_KEY is not set - feedback emails will fail"); | |
| } | |
| const resend = new Resend(process.env.RESEND_API_KEY); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/feedback/route.ts` at line 4, Resend client is initialized
unconditionally which hides missing API key errors; before constructing the
Resend instance (the const resend = new Resend(...) line) validate
process.env.RESEND_API_KEY and fail fast with a clear message or throw an error
(or alternatively check inside the request handler and return a helpful 4xx
error) so that the Resend class is not instantiated with an undefined key;
update the code around the Resend initialization (referencing the Resend symbol
and RESEND_API_KEY env var) to validate and handle the missing key explicitly.
| .navbar__beta-label { | ||
| font-size: 10px; | ||
| font-weight: 600; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.05em; | ||
| color: var(--color-dark-gray); | ||
| background: var(--color-gray-100); | ||
| padding: 1px 6px; | ||
| border-radius: 4px; | ||
| margin-left: 10px; | ||
| line-height: 1; | ||
| } |
There was a problem hiding this comment.
Guideline violations in beta label styling.
Two issues with this CSS:
text-transform: uppercaseon line 186 violates the guideline "do not use uppercase text anywhere."border-radius: 4pxon line 191 should usevar(--radius)per the guideline "Usevar(--radius)(4px) for all border-radius values."
Proposed fix
.navbar__beta-label {
font-size: 10px;
font-weight: 600;
- text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-dark-gray);
background: var(--color-gray-100);
padding: 1px 6px;
- border-radius: 4px;
+ border-radius: var(--radius);
margin-left: 10px;
line-height: 1;
}As per coding guidelines: "do not use uppercase text anywhere" and "Use var(--radius) (4px) for all border-radius values."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .navbar__beta-label { | |
| font-size: 10px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--color-dark-gray); | |
| background: var(--color-gray-100); | |
| padding: 1px 6px; | |
| border-radius: 4px; | |
| margin-left: 10px; | |
| line-height: 1; | |
| } | |
| .navbar__beta-label { | |
| font-size: 10px; | |
| font-weight: 600; | |
| letter-spacing: 0.05em; | |
| color: var(--color-dark-gray); | |
| background: var(--color-gray-100); | |
| padding: 1px 6px; | |
| border-radius: var(--radius); | |
| margin-left: 10px; | |
| line-height: 1; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/globals.css` around lines 183 - 194, The .navbar__beta-label rule
violates styling guidelines: remove or neutralize the uppercase transformation
by deleting text-transform: uppercase (or set text-transform: none) in the
.navbar__beta-label CSS, and replace the hard-coded border-radius: 4px with
border-radius: var(--radius) so the component uses the shared radius variable.
- Fix email typo in .env.local.example (noreply → no-reply) - Add RESEND_API_KEY warning when missing - Add CSRF origin check on feedback POST route - Add role="alert" to error messages for accessibility - Add ARIA attributes to mobile bottom sheet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/app/api/feedback/route.ts (1)
46-65: Consider handling confirmation email failure gracefully.If the support email (lines 32-44) succeeds but the confirmation email fails, the request returns a 500 error even though the primary feedback was delivered. The user may retry and submit duplicate feedback.
Optional: Log confirmation failure without failing the request
// Send confirmation to user if they provided an email if (email) { + try { await resend.emails.send({ from: FROM_EMAIL, to: email, subject: "Thank you for your feedback to Certified.app", text: [ "Thank you for sharing your feedback with us!", "", "We've received the following message:", "", `"${message}"`, "", "We appreciate your input and will review it carefully.", "", "Best regards,", "The Certified Team", ].join("\n"), }); + } catch (confirmError) { + console.error("Confirmation email failed:", confirmError); + // Don't fail the request - feedback was already delivered + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/api/feedback/route.ts` around lines 46 - 65, Wrap the confirmation email send (resend.emails.send using FROM_EMAIL, to: email, subject/body referencing message) in a try/catch so failures to send the confirmation do not cause the whole request to fail; on catch, log the error with a clear message including the email address and error details (using existing logger or console.error) and continue to return the successful response for the primary feedback submission instead of rethrowing the error.src/components/ui/feedback-modal.tsx (1)
262-262: Addaria-hidden="true"to decorative icons.The icons on these lines are decorative (accompanied by text or within buttons with
aria-label) and should be hidden from assistive technology. As per coding guidelines: "aria-hidden=\"true\"on decorative elements."Proposed fix
- <MessageSquare size={16} /> + <MessageSquare size={16} aria-hidden="true" />- {expanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />} + {expanded ? <Minimize2 size={14} aria-hidden="true" /> : <Maximize2 size={14} aria-hidden="true" />}- <X size={16} /> + <X size={16} aria-hidden="true" />Also applies to: 284-284, 292-292
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/feedback-modal.tsx` at line 262, The decorative icon components in the feedback modal (e.g., MessageSquare and the other icon components rendered at the mentioned locations) should be hidden from assistive tech; update each icon element in src/components/ui/feedback-modal.tsx to include aria-hidden="true" (for example <MessageSquare aria-hidden="true" />) and ensure you only add this when the icon is truly decorative (do not add it to icons that convey unique information or are the sole accessible label).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/api/feedback/route.ts`:
- Around line 14-19: The origin/host substring check in route.ts (variables
origin and host, returning NextResponse.json on failure) is vulnerable to
substring/subdomain spoofing; replace the includes(...) logic with a strict
hostname comparison by parsing origin with the URL constructor (or equivalent)
and comparing parsedOrigin.hostname strictly to the request host's hostname
(strip port from host via split or URL) and return 403 if parsing fails or
hostnames do not match; ensure you handle absent headers and wrap URL parsing in
try/catch to reject malformed origins.
In `@src/components/ui/feedback-modal.tsx`:
- Around line 269-277: The dialog ARIA attributes are placed on the backdrop
instead of the modal content; move role="dialog" and aria-modal="true" (and
aria-label if it labels the dialog) from the backdrop div that uses backdropRef
and the onClick handler to the inner container element with className
"feedback-modal" (the element that conditionally applies
feedback-modal--expanded and uses the expanded prop) so screen readers announce
the actual dialog content; keep the backdropRef, onClick logic and styling on
the backdrop div and ensure the inner .feedback-modal receives the accessible
attributes.
---
Nitpick comments:
In `@src/app/api/feedback/route.ts`:
- Around line 46-65: Wrap the confirmation email send (resend.emails.send using
FROM_EMAIL, to: email, subject/body referencing message) in a try/catch so
failures to send the confirmation do not cause the whole request to fail; on
catch, log the error with a clear message including the email address and error
details (using existing logger or console.error) and continue to return the
successful response for the primary feedback submission instead of rethrowing
the error.
In `@src/components/ui/feedback-modal.tsx`:
- Line 262: The decorative icon components in the feedback modal (e.g.,
MessageSquare and the other icon components rendered at the mentioned locations)
should be hidden from assistive tech; update each icon element in
src/components/ui/feedback-modal.tsx to include aria-hidden="true" (for example
<MessageSquare aria-hidden="true" />) and ensure you only add this when the icon
is truly decorative (do not add it to icons that convey unique information or
are the sole accessible label).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8b805549-3e5d-4397-bfe1-2f247d525723
📒 Files selected for processing (3)
.env.local.examplesrc/app/api/feedback/route.tssrc/components/ui/feedback-modal.tsx
✅ Files skipped from review due to trivial changes (1)
- .env.local.example
- Replace substring CSRF origin check with strict hostname comparison - Move dialog ARIA attributes from backdrop to modal content element - Add section 5 (Group and organization accounts) to Terms of Service - Renumber all subsequent ToS sections and update cross-references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Setup
RESEND_API_KEYenv var (already added to Vercel Production + Preview)RESEND_FROM_EMAILoverride (defaults toCertified <no-reply@certified.one>)Test plan
🤖 Generated with Claude Code