feat: add feedback form, sponsor recognition and improve dashboard#334
feat: add feedback form, sponsor recognition and improve dashboard#334ViktorSvertoka merged 2 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdds sponsor detection/display and an in-page feedback form. Introduces new env vars, extends sponsor/user types with email/isSponsor, fetches sponsors for dashboard and leaderboard, updates ProfileCard and leaderboard UI to show sponsor badges/CTAs, and adds a client FeedbackForm that posts to Web3Forms. Changes
Sequence DiagramsequenceDiagram
participant Dashboard as Dashboard Page
participant GitHub as GitHub Sponsors API
participant DB as Leaderboard DB
participant Matcher as Sponsor Matcher
participant UI as ProfileCard / Leaderboard UI
participant User as User
participant Web3Forms as Web3Forms API
Dashboard->>GitHub: getSponsors()
Dashboard->>DB: getLeaderboardData()
GitHub-->>Matcher: Sponsor[] (includes email)
DB-->>Matcher: User[] (includes email)
Matcher-->>Dashboard: annotate users with isSponsor
Dashboard->>UI: render pages with isSponsor flags
User->>UI: open feedback (`#feedback`)
UI->>Web3Forms: POST feedback (uses NEXT_PUBLIC_WEB3FORMS_KEY)
Web3Forms-->>UI: success / error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 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: 4
🤖 Fix all issues with AI agents
In `@frontend/app/`[locale]/dashboard/page.tsx:
- Line 51: The call to getSponsors() in page.tsx triggers uncached GitHub
GraphQL requests because getSponsors() currently uses cache: 'no-store'; update
the implementation in frontend/lib/about/github-sponsors.ts to add server-side
caching (e.g., wrap the fetch logic with Next.js unstable_cache or implement a
simple in-memory/cache-with-TTL) with a reasonable TTL like 1 hour, remove the
cache: 'no-store' option from the GraphQL fetch, and keep the exported function
name (getSponsors) or export a cached wrapper so page.tsx can continue to call
getSponsors() unchanged; ensure errors and fallback behavior remain the same.
- Around line 52-61: The sponsor-matching code can throw when Sponsor fields are
undefined and yields false positives for empty avatarUrl; update the matching
predicate used to set matchedSponsor to defensively handle null/empty values:
check s.login and s.name exist before calling toLowerCase (use optional chaining
or explicit guards), ensure s.name falls back to s.login only if that fallback
is non-empty, and only compare avatar URLs when s.avatarUrl is a non-empty
string (trim and test length > 0) before splitting and calling includes on
userImage; alternatively, enforce non-null defaults in getSponsors() so that
login, name, and avatarUrl are always non-empty strings consistent with the
Sponsor interface.
In `@frontend/components/dashboard/FeedbackForm.tsx`:
- Around line 88-98: The code builds the payload with access_key:
process.env.NEXT_PUBLIC_WEB3FORMS_KEY but lacks a guard when that env var is
undefined; update the submit handler (the function that constructs formData and
the data object in FeedbackForm.tsx) to check NEXT_PUBLIC_WEB3FORMS_KEY before
creating the data object and abort with a clear user-facing error or set an
error state (e.g., show a message/toast) if it's missing; ensure the check
references process.env.NEXT_PUBLIC_WEB3FORMS_KEY and prevents calling the
Web3Forms API when undefined, logging a helpful diagnostic for developers and
returning early from the submit flow.
In `@frontend/components/leaderboard/LeaderboardPodium.tsx`:
- Around line 114-124: The sponsor link currently shows only a decorative Heart
icon on mobile (user.isSponsor block) and lacks an accessible name; add an
aria-label to the anchor element using the localized label (t('sponsor')) so
screen readers announce the link on small screens, and ensure the Heart icon
(Heart component) is marked decorative (aria-hidden=true) if it isn't already so
it won't duplicate the accessible name.
🧹 Nitpick comments (8)
frontend/components/leaderboard/LeaderboardTable.tsx (1)
166-176: Sponsor badge implementation looks good, but consider addingaria-labelfor accessibility.The sponsor badge hides its text label on small screens (
hidden sm:inline), leaving only a small heart icon as the clickable area. Adding anaria-labelto the<a>would improve accessibility for screen reader users and also satisfy touch-target guidelines on mobile.♿ Suggested accessibility improvement
<a href="https://github.com/sponsors/DevLoversTeam" target="_blank" rel="noopener noreferrer" + aria-label={t('sponsor')} className="inline-flex shrink-0 items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-bold text-amber-700 transition-colors hover:bg-amber-200 dark:bg-amber-500/15 dark:text-amber-400 dark:hover:bg-amber-500/25" >frontend/.env.example (1)
64-67: Add a section comment forGITHUB_SPONSORS_TOKEN.It's currently orphaned between the Web3Forms section and the Telegram section without its own header comment, unlike every other variable group in this file.
Suggested grouping
# --- Web3Forms (feedback form) NEXT_PUBLIC_WEB3FORMS_KEY= +# --- GitHub Sponsors GITHUB_SPONSORS_TOKEN=frontend/lib/about/github-sponsors.ts (2)
39-40: Privacy note: fetching sponsor emails for server-side matching.The
69-71: Productionconsole.logon every request.With
cache: 'no-store', thisconsole.logfires on every page load that callsgetSponsors(). Consider downgrading toconsole.debugor removing it to reduce log noise in production.Suggested fix
- console.log( - `✅ GitHub: Found ${rawNodes.length} sponsors for Organization` - ); + if (process.env.NODE_ENV === 'development') { + console.log(`GitHub: Found ${rawNodes.length} sponsors`); + }frontend/components/dashboard/ProfileCard.tsx (1)
66-78: Sponsor badge replaces the role badge entirely.When
isSponsoris true, the user's role (e.g., "Admin", "Member") is hidden. These attributes are orthogonal — a user can be both a sponsor and an admin. Consider showing both badges side-by-side instead of an either/or.Suggested approach
<div className="mt-3 flex flex-wrap items-center gap-2"> - {isSponsor ? ( + {user.role && ( + <span className="inline-flex items-center rounded-full bg-(--accent-primary)/10 px-3 py-1 text-xs font-bold tracking-wider text-(--accent-primary) uppercase"> + {user.role || t('defaultRole')} + </span> + )} + {isSponsor && ( <span className="inline-flex items-center gap-1.5 rounded-full bg-amber-100 px-3 py-1 text-xs font-bold tracking-wider text-amber-700 uppercase dark:bg-amber-500/15 dark:text-amber-400" > <Heart className="h-3 w-3 fill-current" /> {t('sponsor')} </span> - ) : ( - <span className="inline-flex items-center rounded-full bg-(--accent-primary)/10 px-3 py-1 text-xs font-bold tracking-wider text-(--accent-primary) uppercase"> - {user.role || t('defaultRole')} - </span> )} </div>frontend/app/[locale]/leaderboard/page.tsx (1)
22-33: Type system declares these values non-nullable, but consider defensive coding given inconsistencies in the codebase.While
username, and sponsor fields are typed as non-nullable strings, the codebase shows inconsistent null-handling patterns. The dashboard page (line 53) defensively treatsuser.nameas nullable with(user.name ?? ''), yet theUserinterface declares it non-nullable. Additionally,getSponsors()createsSponsorobjects wherenamehave fallbacks, butloginandavatarUrldo not—creating a type mismatch.If the database schema enforces
NOT NULLconstraints and GitHub API always provides these fields, the current code is safe. However, adding optional chaining or nullish coalescing would make the code more robust against runtime data variations:const users = rows.map(({ email, ...user }) => { - const emailLower = email.toLowerCase(); - const nameLower = user.username.toLowerCase(); + const emailLower = (email ?? '').toLowerCase(); + const nameLower = (user.username ?? '').toLowerCase(); const isSponsor = sponsors.some( s => - (s.email && s.email.toLowerCase() === emailLower) || - (nameLower && s.login.toLowerCase() === nameLower) || - (nameLower && s.name.toLowerCase() === nameLower) || + (emailLower && (s.email ?? '').toLowerCase() === emailLower) || + (nameLower && (s.login ?? '').toLowerCase() === nameLower) || + (nameLower && (s.name ?? '').toLowerCase() === nameLower) || (user.avatar && s.avatarUrl && user.avatar.includes((s.avatarUrl ?? '').split('?')[0])) ); return { ...user, isSponsor }; });frontend/components/dashboard/FeedbackForm.tsx (2)
179-218: Custom dropdown lacks keyboard accessibility.The category dropdown cannot be navigated with the keyboard — there's no
Escapeto close, no arrow-key navigation, and noaria-expanded/role="listbox"attributes. This creates an accessibility gap for keyboard and screen-reader users.At minimum, consider adding:
aria-expanded={categoryOpen}androle="combobox"on the trigger buttonrole="listbox"on the dropdown container,role="option"on itemsEscapekey handler to close the dropdown- Arrow key navigation (optional for now, but recommended)
♿ Minimal ARIA and keyboard fix for the trigger button
<button type="button" onClick={() => setCategoryOpen(!categoryOpen)} + aria-expanded={categoryOpen} + aria-haspopup="listbox" + onKeyDown={e => { + if (e.key === 'Escape') setCategoryOpen(false); + }} className={`flex w-full items-center justify-between ...`} >
41-49: Document-level smooth-scroll handler is a broad side effect.This
useEffectregisters a global click listener that intercepts alla[href="#feedback"]clicks across the entire document. While it works for the current layout, it couples this component to the page structure and could interfere with other anchors if the pattern changes. A more contained approach would be to handle the scroll via anonClickon the anchor element in the parent page (though that's a server component, so this is understandably a workaround).Worth a brief inline comment explaining why this global listener exists to help future maintainers.
|
|
||
| const t = await getTranslations('dashboard'); | ||
|
|
||
| const sponsors = await getSponsors(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find where getSponsors is defined
fd . frontend/app -name "*.ts" -o -name "*.tsx" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 301
🏁 Script executed:
# Check the dashboard page file
cat -n frontend/app/[locale]/dashboard/page.tsxRepository: DevLoversTeam/devlovers.net
Length of output: 6101
🏁 Script executed:
# Search for getSponsors definition
rg "getSponsors|github-sponsors" --type ts --type tsxRepository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
# Find the github-sponsors file
find . -path "./lib/about/github-sponsors*" -type fRepository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
# Also try searching with different extensions
fd "github-sponsors" --extension ts --extension tsxRepository: DevLoversTeam/devlovers.net
Length of output: 108
🏁 Script executed:
# Check lib/about directory structure
ls -la lib/about/ 2>/dev/null || ls -la src/lib/about/ 2>/dev/null || find . -name "about" -type d | grep libRepository: DevLoversTeam/devlovers.net
Length of output: 91
🏁 Script executed:
# Search for getSponsors function definition
rg "export.*getSponsors|function getSponsors" -t tsRepository: DevLoversTeam/devlovers.net
Length of output: 166
🏁 Script executed:
cat -n frontend/lib/about/github-sponsors.tsRepository: DevLoversTeam/devlovers.net
Length of output: 3446
Add caching to the getSponsors() call to prevent GitHub API rate limiting.
The getSponsors() function explicitly disables caching with cache: 'no-store' (line 56 of frontend/lib/about/github-sponsors.ts). Since it's called on every dashboard page load and fetches from the GitHub GraphQL API, this will incur unnecessary requests and risks hitting GitHub's rate limits under load. Implement Next.js unstable_cache with a reasonable TTL (e.g., 1 hour), or use a manual cache since sponsor data changes infrequently.
🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/dashboard/page.tsx at line 51, The call to
getSponsors() in page.tsx triggers uncached GitHub GraphQL requests because
getSponsors() currently uses cache: 'no-store'; update the implementation in
frontend/lib/about/github-sponsors.ts to add server-side caching (e.g., wrap the
fetch logic with Next.js unstable_cache or implement a simple
in-memory/cache-with-TTL) with a reasonable TTL like 1 hour, remove the cache:
'no-store' option from the GraphQL fetch, and keep the exported function name
(getSponsors) or export a cached wrapper so page.tsx can continue to call
getSponsors() unchanged; ensure errors and fallback behavior remain the same.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@frontend/components/dashboard/FeedbackForm.tsx`:
- Around line 100-108: The honeypot value is being discarded because the data
object sets botcheck to a hardcoded empty string; update the code that builds
the payload (the data object in FeedbackForm.tsx) to read the honeypot value
from the form via formData.get('botcheck') (or the exact honeypot field name)
instead of '' so the backend can detect bots; ensure the key name remains
botcheck to match server-side validation and handle null by defaulting to '' if
needed.
🧹 Nitpick comments (4)
frontend/components/dashboard/FeedbackForm.tsx (4)
41-49: Global smooth-scroll listener is a cross-cutting concern owned by the wrong component.This
FeedbackFormcomponent intercepts alla[href="#feedback"]clicks document-wide. This is a page-level routing concern, not a form responsibility. It also callse.preventDefault(), which prevents the URL hash from updating — breaking the "copy link" and back-button expectations for the#feedbackanchor.Consider moving this to the page or a dedicated scroll utility, or simply adding
scroll-behavior: smoothvia CSS on the scroll container.
188-222: Custom dropdown lacks keyboard navigation and ARIA attributes.The category selector is a custom dropdown but is missing:
aria-expandedon the trigger buttonrole="listbox"on the options containerrole="option"on each option- Keyboard navigation (arrow keys, Enter, Escape)
This makes the dropdown inaccessible to keyboard-only users and screen readers. Consider using a
<select>element for native accessibility, or add proper ARIA attributes and keyboard handlers.♿ Minimal ARIA improvements on the trigger button
<button type="button" onClick={() => setCategoryOpen(!categoryOpen)} + aria-haspopup="listbox" + aria-expanded={categoryOpen} className={`flex w-full items-center ...`} >
110-131: Non-OK HTTP responses (4xx/5xx) are not checked before parsing JSON.If Web3Forms returns a non-2xx status (e.g., 429 rate limit),
res.json()may still succeed butresult.successwould be false, which is handled. However, if the response body isn't valid JSON (e.g., an HTML error page from a proxy),res.json()will throw — which thecatchblock handles by setting'error'status. This is acceptable, but ares.okcheck before parsing would provide cleaner error handling.Proposed improvement
const res = await fetch('https://api.web3forms.com/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(data), }); + if (!res.ok) { + setStatus('error'); + return; + } + const result = await res.json();
156-159: Success state hides the form entirely — user must wait 5 seconds to submit again.After successful submission, the form is replaced with a success message for 5 seconds (Line 123 sets a timer). During that window, the user cannot submit another feedback. This is reasonable rate-limiting UX, but consider adding a "Send another" link/button so users don't have to wait.
| const data = { | ||
| access_key: accessKey, | ||
| subject: `DevLovers Feedback: ${formData.get('category')}`, | ||
| from_name: formData.get('name'), | ||
| email: formData.get('email'), | ||
| category: formData.get('category'), | ||
| message: formData.get('message'), | ||
| botcheck: '', | ||
| }; |
There was a problem hiding this comment.
Honeypot field is ineffective — botcheck is hardcoded to empty string.
The hidden honeypot input (Line 162) is meant to catch bots that auto-fill all fields. However, on Line 107, botcheck is hardcoded to '' instead of reading from formData. A bot filling the honeypot field will have its value silently discarded, defeating the spam protection.
🐛 Proposed fix — read the honeypot value from formData
const data = {
access_key: accessKey,
subject: `DevLovers Feedback: ${formData.get('category')}`,
from_name: formData.get('name'),
email: formData.get('email'),
category: formData.get('category'),
message: formData.get('message'),
- botcheck: '',
+ botcheck: formData.get('botcheck') ?? '',
};📝 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 data = { | |
| access_key: accessKey, | |
| subject: `DevLovers Feedback: ${formData.get('category')}`, | |
| from_name: formData.get('name'), | |
| email: formData.get('email'), | |
| category: formData.get('category'), | |
| message: formData.get('message'), | |
| botcheck: '', | |
| }; | |
| const data = { | |
| access_key: accessKey, | |
| subject: `DevLovers Feedback: ${formData.get('category')}`, | |
| from_name: formData.get('name'), | |
| email: formData.get('email'), | |
| category: formData.get('category'), | |
| message: formData.get('message'), | |
| botcheck: formData.get('botcheck') ?? '', | |
| }; |
🤖 Prompt for AI Agents
In `@frontend/components/dashboard/FeedbackForm.tsx` around lines 100 - 108, The
honeypot value is being discarded because the data object sets botcheck to a
hardcoded empty string; update the code that builds the payload (the data object
in FeedbackForm.tsx) to read the honeypot value from the form via
formData.get('botcheck') (or the exact honeypot field name) instead of '' so the
backend can detect bots; ensure the key name remains botcheck to match
server-side validation and handle null by defaulting to '' if needed.
Summary by CodeRabbit
New Features
Style
Chores