Skip to content

feat: add feedback form, sponsor recognition and improve dashboard#334

Merged
ViktorSvertoka merged 2 commits intodevelopfrom
feat/user-feedback-system
Feb 16, 2026
Merged

feat: add feedback form, sponsor recognition and improve dashboard#334
ViktorSvertoka merged 2 commits intodevelopfrom
feat/user-feedback-system

Conversation

@TiZorii
Copy link
Collaborator

@TiZorii TiZorii commented Feb 16, 2026

Summary by CodeRabbit

  • New Features

    • Added in-app feedback form for bugs, suggestions, and questions
    • GitHub Sponsors support with sponsor recognition and badges across profile, leaderboard, and podium
  • Style

    • Refined dashboard card borders, padding, and profile card layout for improved visual hierarchy
    • Enhanced quiz results section header and trophy styling
  • Chores

    • Leaderboard now shows top 20 users
    • Added localization for sponsorship and feedback flows in multiple languages

@vercel
Copy link
Contributor

vercel bot commented Feb 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
devlovers-net Ready Ready Preview, Comment Feb 16, 2026 7:39am

@netlify
Copy link

netlify bot commented Feb 16, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit c35e37c
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/6992c9552b908e0008bc1bf6
😎 Deploy Preview https://deploy-preview-334--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Environment
frontend/.env.example
Added NEXT_PUBLIC_WEB3FORMS_KEY and GITHUB_SPONSORS_TOKEN.
Sponsor data & types
frontend/lib/about/github-sponsors.ts, frontend/components/leaderboard/types.ts, frontend/db/queries/leaderboard.ts
Added sponsor email in GraphQL mapping and Sponsor type; added email to leaderboard rows; added optional isSponsor?: boolean to User type; updated leaderboard queries to return email.
Dashboard page & feedback
frontend/app/[locale]/dashboard/page.tsx, frontend/components/dashboard/FeedbackForm.tsx, frontend/components/dashboard/ProfileCard.tsx
Dashboard now fetches sponsors and matches users to set isSponsor; added FeedbackForm component and feedback anchor; ProfileCard gains isSponsor prop and renders sponsor badge/CTA.
Leaderboard page & components
frontend/app/[locale]/leaderboard/page.tsx, frontend/components/leaderboard/LeaderboardPodium.tsx, frontend/components/leaderboard/LeaderboardTable.tsx
Parallelized data fetching (users + sponsors); injects isSponsor per user; adds Heart sponsor badges; expanded table from top 10 → top 20.
Dashboard UI styling
frontend/components/dashboard/ExplainedTermsCard.tsx, frontend/components/dashboard/QuizResultsSection.tsx, frontend/components/dashboard/StatsCard.tsx, frontend/components/dashboard/QuizSavedBanner.tsx, frontend/components/dashboard/QuizResultRow.tsx
Multiple Tailwind class adjustments: border color/padding tweaks, token syntax changes, header enhancement (trophy + subtitle) in QuizResultsSection, and minor layout rearrangements.
Localization
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added translations for sponsor labels, sponsor CTAs, quizResults subtitle, and a full dashboard.feedback block for the feedback form.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 Badges glow and hearts take flight,
I nibble feedback through the night,
Sponsors sparkle on the board,
A tiny form, a friendly word,
Hops of joy — the site feels light.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes in the PR: adding a feedback form component, implementing sponsor recognition throughout the dashboard and leaderboard, and various dashboard UI improvements.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user-feedback-system

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 adding aria-label for 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 an aria-label to 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 for GITHUB_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 email field from GitHub's GraphQL API returns only the user's public email, and many users leave this blank. This means email-based sponsor matching will only work for sponsors who have configured a public email on GitHub. Consider documenting this limitation so maintainers understand why some sponsors may not be matched.


69-71: Production console.log on every request.

With cache: 'no-store', this console.log fires on every page load that calls getSponsors(). Consider downgrading to console.debug or 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 isSponsor is 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 email, username, and sponsor fields are typed as non-nullable strings, the codebase shows inconsistent null-handling patterns. The dashboard page (line 53) defensively treats user.name as nullable with (user.name ?? ''), yet the User interface declares it non-nullable. Additionally, getSponsors() creates Sponsor objects where email and name have fallbacks, but login and avatarUrl do not—creating a type mismatch.

If the database schema enforces NOT NULL constraints 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 Escape to close, no arrow-key navigation, and no aria-expanded/role="listbox" attributes. This creates an accessibility gap for keyboard and screen-reader users.

At minimum, consider adding:

  • aria-expanded={categoryOpen} and role="combobox" on the trigger button
  • role="listbox" on the dropdown container, role="option" on items
  • Escape key 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 useEffect registers a global click listener that intercepts all a[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 an onClick on 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find where getSponsors is defined
fd . frontend/app -name "*.ts" -o -name "*.tsx" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 301


🏁 Script executed:

# Check the dashboard page file
cat -n frontend/app/[locale]/dashboard/page.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 6101


🏁 Script executed:

# Search for getSponsors definition
rg "getSponsors|github-sponsors" --type ts --type tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Find the github-sponsors file
find . -path "./lib/about/github-sponsors*" -type f

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Also try searching with different extensions
fd "github-sponsors" --extension ts --extension tsx

Repository: 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 lib

Repository: DevLoversTeam/devlovers.net

Length of output: 91


🏁 Script executed:

# Search for getSponsors function definition
rg "export.*getSponsors|function getSponsors" -t ts

Repository: DevLoversTeam/devlovers.net

Length of output: 166


🏁 Script executed:

cat -n frontend/lib/about/github-sponsors.ts

Repository: 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FeedbackForm component intercepts all a[href="#feedback"] clicks document-wide. This is a page-level routing concern, not a form responsibility. It also calls e.preventDefault(), which prevents the URL hash from updating — breaking the "copy link" and back-button expectations for the #feedback anchor.

Consider moving this to the page or a dedicated scroll utility, or simply adding scroll-behavior: smooth via 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-expanded on the trigger button
  • role="listbox" on the options container
  • role="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 but result.success would 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 the catch block handles by setting 'error' status. This is acceptable, but a res.ok check 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.

Comment on lines +100 to +108
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: '',
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants