feat: public community page — leaderboards, showcases, and stats#120
feat: public community page — leaderboards, showcases, and stats#120BillChirico merged 24 commits intomainfrom
Conversation
|
Claude finished @BillChirico's task in 3m 1s —— View job Review Complete — 1 warning, 1 documentation issue, 1 nitpick
Most prior review feedback has been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces, activeProjects filter, test mock). Three issues remain: 🟡 🟡 Duplicated 🟡 AGENTS.md not updated — Key Files table missing new files per project conventions. 🔵 Badge name inconsistency — |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds public community features including a database migration enabling opt-in public profiles, new backend API routes for leaderboards, showcases, community statistics, and user profiles with rate limiting, frontend community hub and user profile pages, and comprehensive test coverage. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @BillChirico. The following files were modified: * `src/api/routes/community.js` * `web/src/app/community/[guildId]/[userId]/page.tsx` * `web/src/app/community/[guildId]/page.tsx` These files were ignored: * `tests/api/routes/community.test.js`
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
| Filename | Overview |
|---|---|
| src/api/routes/community.js | Public API endpoints with proper rate limiting, parameterized queries, privacy enforcement, and Winston logging |
| tests/api/routes/community.test.js | Comprehensive test coverage (27 tests) covering all endpoints, privacy, rate limiting, and error cases |
| web/src/app/community/[guildId]/[userId]/page.tsx | SSR profile page with proper types and SEO; one hardcoded locale needs fixing |
Sequence Diagram
sequenceDiagram
participant User as Browser
participant Next as Next.js SSR
participant API as Express API
participant DB as PostgreSQL
participant Discord as Discord.js Client
User->>Next: GET /community/[guildId]
Next->>API: GET /community/:guildId/stats
Next->>API: GET /community/:guildId/leaderboard
Next->>API: GET /community/:guildId/showcases
par Fetch Stats
API->>DB: Query user_stats, reputation<br/>(public_profile = TRUE)
DB-->>API: Community stats
API->>Discord: Bulk fetch top 3 members
Discord-->>API: Member metadata
and Fetch Leaderboard
API->>DB: Query top 25 by XP<br/>(public_profile = TRUE)
DB-->>API: User rankings
API->>Discord: Bulk fetch members
Discord-->>API: Avatars, usernames
and Fetch Showcases
API->>DB: Query showcases + JOIN user_stats<br/>(public_profile = TRUE)
DB-->>API: Projects
API->>Discord: Bulk fetch authors
Discord-->>API: Author metadata
end
API-->>Next: JSON responses (rate limited 30/min)
Next-->>User: Rendered community page
User->>Next: Click member profile
Next->>API: GET /community/:guildId/profile/:userId
API->>DB: Check public_profile = TRUE
alt Profile is public
API->>DB: Fetch stats, reputation, showcases
DB-->>API: User data
API->>Discord: Fetch member info
Discord-->>API: Avatar, username
API-->>Next: Profile JSON
Next-->>User: Rendered profile page
else Profile is private
API-->>Next: 404 Profile not found
Next-->>User: 404 page
end
Last reviewed commit: c658ba9
There was a problem hiding this comment.
Review Summary
2 critical issues, 1 nitpick found.
🔴 Critical: Leaderboard → Profile links are completely broken
The leaderboard API (src/api/routes/community.js:119-128) does not return user_id in its response. The frontend community page (web/src/app/community/[guildId]/page.tsx:338) builds profile links using member.username (e.g. /community/guild123/alice). However, the profile page route is /community/[guildId]/[userId] and the profile API endpoint queries by Discord user ID (snowflake). Since a username like "alice" will never match a snowflake like "123456789012345678", every profile link from the leaderboard will 404.
Fix: Add userId: row.user_id to the leaderboard response, update the LeaderboardMember TypeScript interface, and change the frontend link to use member.userId.
🟡 Warning: AGENTS.md not updated
Per the project's own documentation requirements, the Key Files table in AGENTS.md must be updated when adding new files/modules. The following are missing:
src/api/routes/community.jsweb/src/app/community/[guildId]/page.tsxweb/src/app/community/[guildId]/[userId]/page.tsxmigrations/013_public_profiles.cjs
🔵 Nitpick: Duplicated code across frontend pages
getApiBase(), API_BASE, XpBar, and StatCard are duplicated between the community page and the profile page. Could be extracted to a shared module.
📋 Prompt to fix all issues
Fix the following issues in the volvox-bot repository on branch feat/community-page:
1. FILE: src/api/routes/community.js, LINES 119-128
ISSUE: The leaderboard response object is missing `user_id`. Add `userId: row.user_id` as the first property in the returned object (before `username`).
2. FILE: web/src/app/community/[guildId]/page.tsx, LINE 9-17
ISSUE: The LeaderboardMember interface is missing `userId`. Add `userId: string;` as the first property.
3. FILE: web/src/app/community/[guildId]/page.tsx, LINE 338
ISSUE: Profile link uses `member.username` but needs to use `member.userId`. Change:
href={`/community/${guildId}/${member.username}`}
to:
href={`/community/${guildId}/${member.userId}`}
4. FILE: AGENTS.md
ISSUE: Key Files table is missing new files. Add these rows to the Key Files table:
- `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
- `web/src/app/community/[guildId]/page.tsx` | Community hub page — SSR stats banner, leaderboard, showcase gallery
- `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile page — SSR avatar, level, XP, stats, badges, projects
- `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats
5. OPTIONAL (nitpick): Extract duplicated `getApiBase()`, `API_BASE`, `XpBar`, and `StatCard` from the two community page files into a shared module like `web/src/lib/community-utils.ts` or `web/src/app/community/shared.tsx`.
After changes, run `npx biome check --write` and `pnpm test` to verify.
There was a problem hiding this comment.
Pull request overview
Implements a public-facing “Community Hub” feature (no login) by adding new community API endpoints, SSR community/profile pages in the web app, and a DB opt-in flag to ensure only users with public profiles are exposed.
Changes:
- Added public
/api/v1/community/*endpoints (leaderboard, showcases, stats, public profile) with aggressive rate limiting. - Added SSR community hub and public profile pages under
/community/[guildId]and/community/[guildId]/[userId]with SEO metadata. - Added a migration introducing
user_stats.public_profile(default false) and tests covering endpoints, privacy enforcement, and rate limiting.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/app/community/[guildId]/page.tsx | SSR community hub page rendering stats, leaderboard, and showcase gallery. |
| web/src/app/community/[guildId]/[userId]/page.tsx | SSR public profile page with stats, badges, and project list + metadata. |
| src/api/routes/community.js | New public community router with rate limiting and privacy gating for leaderboard/profile. |
| src/api/index.js | Mounts the new community router as a public route group. |
| migrations/013_public_profiles.cjs | Adds public_profile opt-in flag + partial index for public queries. |
| tests/api/routes/community.test.js | Adds route tests for correctness, privacy behavior, error handling, and rate limiting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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/api/routes/community.js`:
- Around line 119-127: The leaderboard response is missing a stable user
identifier; update the returned object in the leaderboard mapping (the block
that currently returns username, displayName, avatar, xp, level, badge:
getLevelBadge(level), rank: offset + idx + 1) to include the persistent user id
(e.g., userId or user_id) coming from the query row (e.g., row.user_id or
row.userId) so downstream profile routing that resolves by user_id works
reliably; ensure the property name matches what the frontend expects (userId or
user_id) and use the same row field used in the SQL SELECT.
- Around line 164-171: The showcases query currently returns all rows for a
guild regardless of users' privacy settings; update the SQL used in the route
handler that builds the two queries (the COUNT query and the SELECT query using
pool.query/ORDER BY ${orderBy}) to only include showcases whose authors have
opted into public profiles by joining the user_stats (or users) table and adding
a WHERE clause like user_stats.public_profile = true (or equivalent) for both
the COUNT query and the SELECT query; ensure the JOIN targets the same author_id
used in the SELECT (e.g., JOIN user_stats us ON s.author_id = us.user_id) so
counts and paging remain consistent with the filtered result set.
- Around line 99-113: The current Promise.all loop maps membersResult.rows and
calls guild.members.fetch(row.user_id) per member (in the block that computes
level via computeLevel and builds username/displayName/avatar), causing many
individual API requests; change this to batch-fetch all user IDs once using
guild.members.fetch({ user: userIdsArray }) (as done in
src/commands/leaderboard.js), store the returned Map, then inside the
membersResult.rows.map use map.get(row.user_id) to populate username,
displayName, and avatar (falling back to row.user_id when missing) so you
replace per-row fetches with a single bulk fetch and efficient lookups.
In `@tests/api/routes/community.test.js`:
- Around line 159-220: Add a new test in the GET
/api/v1/community/:guildId/showcases suite that verifies privacy filtering:
mockPool.query should first return total rows and then a projects array
containing one project by PUBLIC_USER (opted-in) and one by PRIVATE_USER (not
opted-in); call request(app).get(`/api/v1/community/${GUILD_ID}/showcases`) and
assert the response only includes the opted-in project (res.body.projects length
is 1, contains the PUBLIC_USER project fields and authorName 'Alice') and that
the PRIVATE_USER project is excluded; use the existing constants PUBLIC_USER and
PRIVATE_USER and inspect mockPool.query.mock.calls to ensure the DB query was
executed for the showcases endpoint.
In `@web/src/app/community/`[guildId]/[userId]/page.tsx:
- Around line 240-244: Replace the raw <img> used to render the avatar with
Next.js's Image component: import Image from 'next/image' at the top of the
file, then swap the <img src={profile.avatar} alt={profile.displayName}
className="h-24 w-24 rounded-full object-cover ring-2 ring-primary/20" /> usage
to an <Image> that uses profile.avatar and profile.displayName and supplies
either explicit width/height (e.g., 96x96 matching h-24/w-24) or uses
fill/layout with appropriate objectFit to preserve the rounded/full styling;
ensure the same classes (rounded-full, ring-2, ring-primary/20) remain applied
and remove any unsupported props for Image.
In `@web/src/app/community/`[guildId]/page.tsx:
- Around line 201-209: XpBar currently computes progress from a hardcoded
thresholds array causing wrong/negative widths; update XpBar({ xp, level }) to
instead accept and use API-provided values (e.g., currentThreshold,
nextThreshold or a precomputed progressPercent) and compute progress from those
fields rather than the local thresholds constant; modify the call sites that
render XpBar to pass the API's threshold/progress fields returned by the
community endpoints (the same change must also be applied to the XpBar usage in
the component in the [userId] page) so both UI and API use the same level
thresholds and avoid negative/incorrect widths.
- Around line 246-247: Replace the raw <img> in the MemberAvatar component with
Next.js's Image component: import Image from 'next/image', use <Image
src={avatar} alt={name} width={sizePx} height={sizePx}
className={`${sizeClasses[size]} rounded-full object-cover`} /> (or provide
appropriate numeric width/height derived from your size mapping) and remove the
plain img; ensure src is a valid string or handled with next/image compatible
loader/config and preserve the same className/alt semantics so Next's
optimization and repo lint rules are satisfied.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
migrations/013_public_profiles.cjssrc/api/index.jssrc/api/routes/community.jstests/api/routes/community.test.jsweb/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: claude-review
- GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')
Files:
src/api/index.jssrc/api/routes/community.jstests/api/routes/community.test.js
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome
Files:
src/api/index.jssrc/api/routes/community.jstests/api/routes/community.test.jsweb/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
src/**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit
Files:
src/api/index.jssrc/api/routes/community.js
web/**/*.{tsx,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/image Image component with appropriate layout and sizing props in Next.js components
Files:
web/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores
Files:
web/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
🧬 Code graph analysis (2)
tests/api/routes/community.test.js (1)
src/api/server.js (1)
createApp(27-87)
web/src/app/community/[guildId]/[userId]/page.tsx (4)
web/src/components/ui/card.tsx (5)
Card(55-55)CardContent(55-55)CardHeader(55-55)CardTitle(55-55)CardDescription(55-55)src/commands/rank.js (1)
currentThreshold(57-57)src/api/routes/community.js (1)
level(352-352)web/src/components/ui/badge.tsx (1)
Badge(46-46)
🔇 Additional comments (2)
migrations/013_public_profiles.cjs (1)
10-27: Migration structure looks solid and rollback-safe.
up/downare symmetric, idempotent (IF NOT EXISTS/IF EXISTS), and aligned with the feature’s privacy model.src/api/index.js (1)
22-24: Public community routing placement is correct.Mounting
/communityas a public route here is consistent with the feature goals and keeps auth-protected groups intact.
…avoid rate limits
There was a problem hiding this comment.
Review Summary — 1 warning, 1 documentation issue
Most issues from prior reviews have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, test mock). Two issues remain:
🟡 Warning
- Stats
activeProjectscount inconsistent with showcases endpoint (community.js:251): The/showcasesendpoint correctly filters bypublic_profile = TRUE, but the/statsproject count queries all showcases without the filter. The stats banner may show a higher project count than what actually appears on the showcases page. See inline comment for fix.
🟡 Documentation
- AGENTS.md Key Files table not updated: Per project conventions (AGENTS.md line 177, 183–184), new files must be added to the Key Files table. Missing entries:
src/api/routes/community.js— Public community API (leaderboard, showcases, stats, profile; no auth, rate-limited)web/src/app/community/[guildId]/page.tsx— Community hub SSR pageweb/src/app/community/[guildId]/[userId]/page.tsx— Public profile SSR pagemigrations/013_public_profiles.cjs— Addspublic_profileboolean touser_stats
Prompt to fix all issues
Fix the following issues on branch feat/community-page:
1. FILE: src/api/routes/community.js, LINE 251
ISSUE: Stats activeProjects count doesn't filter by public_profile, inconsistent with showcases endpoint.
FIX: Replace the plain showcases count query with one that joins user_stats and filters by public_profile = TRUE:
pool.query(
`SELECT COUNT(*)::int AS count
FROM showcases s
INNER JOIN user_stats us ON us.guild_id = s.guild_id AND us.user_id = s.author_id
WHERE s.guild_id = $1 AND us.public_profile = TRUE`,
[guildId],
),
2. FILE: AGENTS.md
ISSUE: Key Files table is missing new files added in this PR.
FIX: Add these rows to the Key Files table (in the appropriate position alphabetically):
- `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
- `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
- `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
- `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index
After changes, run: pnpm test
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/src/app/community/`[guildId]/[userId]/page.tsx:
- Around line 170-177: The XpBar component currently computes progress using a
hardcoded thresholds array in the XpBar function (variable thresholds), which
can disagree with guild-specific level thresholds defined in
repConfig.levelThresholds; update the component to consume threshold values
computed by the API instead of local constants — have the API include
currentThreshold and nextThreshold (or a precomputed progressPercent) with the
returned xp/level, then change XpBar to accept those props (e.g.,
currentThreshold/nextThreshold or progressPercent) and compute progress from
them rather than the local thresholds array.
In `@web/src/app/community/`[guildId]/page.tsx:
- Around line 390-391: Replace the unstable array index key used in the top
contributors list with a stable unique id: in the map over stats.topContributors
(the expression using contributor and idx) change the element key from idx to
the contributor's unique identifier (contributor.userId) so React can reconcile
items reliably.
ℹ️ Review info
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
migrations/013_public_profiles.cjssrc/api/index.jssrc/api/routes/community.jstests/api/routes/community.test.jsweb/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Greptile Review
- GitHub Check: Agent
- GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')
Files:
src/api/routes/community.jstests/api/routes/community.test.jssrc/api/index.js
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome
Files:
src/api/routes/community.jsweb/src/app/community/[guildId]/[userId]/page.tsxtests/api/routes/community.test.jssrc/api/index.jsweb/src/app/community/[guildId]/page.tsx
src/**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit
Files:
src/api/routes/community.jssrc/api/index.js
web/**/*.{tsx,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use next/image Image component with appropriate layout and sizing props in Next.js components
Files:
web/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
web/src/**/*.{tsx,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores
Files:
web/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
🧠 Learnings (2)
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to web/**/*.{tsx,ts} : Use next/image Image component with appropriate layout and sizing props in Next.js components
Applied to files:
web/src/app/community/[guildId]/[userId]/page.tsxweb/src/app/community/[guildId]/page.tsx
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit
Applied to files:
tests/api/routes/community.test.js
🧬 Code graph analysis (4)
src/api/routes/community.js (3)
src/api/middleware/rateLimit.js (1)
rateLimit(15-56)src/modules/config.js (2)
getConfig(282-313)err(94-94)src/modules/reputation.js (1)
computeLevel(48-58)
web/src/app/community/[guildId]/[userId]/page.tsx (5)
web/src/components/ui/card.tsx (5)
Card(55-55)CardContent(55-55)CardHeader(55-55)CardTitle(55-55)CardDescription(55-55)src/commands/rank.js (1)
currentThreshold(57-57)src/api/routes/community.js (1)
level(361-361)web/src/components/ui/badge.tsx (1)
Badge(46-46)src/commands/profile.js (1)
badge(83-83)
tests/api/routes/community.test.js (1)
src/api/server.js (1)
createApp(27-87)
src/api/index.js (1)
src/api/routes/community.js (1)
router(16-16)
🪛 GitHub Actions: CI
web/src/app/community/[guildId]/page.tsx
[error] 391-391: lint/suspicious/noArrayIndexKey: Avoid using the index of an array as key property in an element.
[warning] 396-399: Formatter would have printed content changes (formatting differences detected).
🔇 Additional comments (11)
src/api/index.js (1)
9-9: LGTM!The community router is correctly mounted as a public route before authenticated routes. The placement after
/healthand before/authis appropriate for an unauthenticated, rate-limited endpoint group.Also applies to: 22-24
migrations/013_public_profiles.cjs (1)
1-27: LGTM!The migration is well-designed:
- Privacy-by-default with
DEFAULT FALSE- Idempotent with
IF NOT EXISTS/IF EXISTSguards- Partial index efficiently supports queries filtering
public_profile = TRUE- Down migration correctly drops index before column
web/src/app/community/[guildId]/[userId]/page.tsx (1)
1-84: LGTM!Data fetching logic is well-structured with proper error handling, revalidation, and the
Imagecomponent fromnext/imageis correctly used for avatar rendering (addressing the previous review comment).web/src/app/community/[guildId]/page.tsx (2)
205-213: Hardcoded XP thresholds may mismatch guild-specific configuration.Same issue as in
[userId]/page.tsx: theXpBarcomponent uses fixed thresholds while the API uses guild-configurable values.
236-271: LGTM!
MemberAvatarcorrectly usesnext/imagewith appropriatewidthandheightprops derived from the size mapping, addressing the previous review comment.tests/api/routes/community.test.js (2)
397-434: LGTM!The showcases privacy regression test properly validates that:
- SQL queries include
public_profile = TRUEfiltering- Only public users' projects appear in responses
- Private users' projects are excluded
This addresses the previous review comment about missing privacy coverage for showcases.
582-603: LGTM!Rate limiting test correctly validates the 30 req/min limit by sending 31 requests and asserting at least one receives a 429 response. Placement at the end of the suite prevents rate limit state from affecting other tests.
src/api/routes/community.js (4)
61-137: LGTM!The leaderboard endpoint correctly:
- Filters by
public_profile = TRUEin both count and data queries- Uses batch member fetching (
guild.members.fetch({ user: userIds }))- Includes
userIdin the response for stable profile routing- Applies sensible pagination limits (1-100)
145-222: LGTM!The showcases endpoint properly enforces privacy by joining
user_statsand filteringus.public_profile = TRUEin both count and data queries. Batch member fetching is correctly implemented.
229-312: LGTM!The stats endpoint appropriately:
- Filters
memberCountandtopContributorsbypublic_profile = TRUE- Uses aggregate counts for
messagesThisWeek,activeProjects, andchallengesCompleted(no per-user privacy concern)- Implements batch fetching for top contributors
320-438: LGTM!The profile endpoint correctly validates
public_profileopt-in before returning any user data, returns 404 for non-opted-in users, and assembles a comprehensive response with badges, projects, and stats.
The stats endpoint counted ALL showcases for the guild, but the showcases endpoint only returns entries from users with public_profile=TRUE. This caused the stats banner to show a higher project count than the gallery actually displayed. Added INNER JOIN on user_stats with public_profile=TRUE filter so the count stays consistent with what the showcases endpoint exposes. Resolves review thread PRRT_kwDORICdSM5xTk2J
Expose currentLevelXp/nextLevelXp in /community/:guildId/profile/:userId and update profile XpBar to consume these values instead of hardcoded defaults. This keeps profile progress bars consistent with guild-specific reputation thresholds.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Review Summary — 1 warning, 1 documentation issue
Most issues from prior review rounds have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces). Two issues remain:
🟡 Warning: messagesThisWeek stat is semantically incorrect (community.js:252-256)
messages_sent is a running lifetime total (confirmed in engagement.js:35: messages_sent = user_stats.messages_sent + 1). The query sums ALL-TIME messages_sent for users with last_active >= NOW() - '7 days'. The UI labels this "Messages This Week" which is inaccurate — it's actually "total lifetime messages from recently active users." See inline comment for fix options.
🟡 Documentation: AGENTS.md Key Files table not updated
Per project conventions (AGENTS.md lines 183-184), new files must be added to the Key Files table. Missing entries:
src/api/routes/community.jsweb/src/app/community/[guildId]/page.tsxweb/src/app/community/[guildId]/[userId]/page.tsxmigrations/013_public_profiles.cjs
Prompt to fix all issues
Fix the following issues on branch feat/community-page:
1. FILE: src/api/routes/community.js, LINES 252-256
ISSUE: messagesThisWeek stat sums ALL-TIME messages_sent for recently active users, not messages sent this week. The label "Messages This Week" is misleading.
FIX: Either rename the API field and frontend label to accurately describe what it measures (e.g., "Messages by Active Members"), OR if there's a messages/events table with timestamps, query actual weekly message counts instead.
2. FILE: AGENTS.md
ISSUE: Key Files table is missing new files added in this PR.
FIX: Add these rows to the Key Files table:
- `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
- `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
- `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
- `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index
After changes, run: npx biome check --write && pnpm test
The messages_sent column is a lifetime running total, not a weekly count. Summing it for recently-active users is misleading when labeled 'Messages This Week'. Rename the API response field to totalMessagesSent and update the frontend CommunityStats interface and display label accordingly.
There was a problem hiding this comment.
Review Summary — 1 warning, 1 documentation issue, 1 nitpick
Most issues from prior review rounds have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces, activeProjects filter, test mock). Three issues remain:
🟡 Warning: totalMessagesSent query is misleading (community.js:252-256)
The query sums ALL-TIME messages_sent for users with last_active >= NOW() - '7 days'. The field name and UI label both say "Total Messages Sent" implying a community-wide total, but users inactive for 8+ days are excluded. Remove the 7-day filter to match the semantics. See inline comment.
🟡 Documentation: Duplicated getApiBase() / API_BASE (page.tsx:54-66, [userId]/page.tsx:54-67)
Both community pages re-implement API URL normalization that already exists as getBotApiBaseUrl() in web/src/lib/bot-api.ts. The shared helper also returns null when BOT_API_URL is unset (safer for production) rather than silently falling back to http://localhost:3001.
🟡 Documentation: AGENTS.md Key Files table not updated
Per project conventions (AGENTS.md lines 183-184), new files must be added. Missing entries: community.js, both page.tsx files, and the migration.
🔵 Nitpick: Badge name inconsistency (community.js:418)
🗣️ Active Voice with description 100+ messages — name implies voice activity but it's message-based. PR author acknowledged for follow-up.
Prompt to fix all issues
Fix the following issues on branch feat/community-page:
1. FILE: src/api/routes/community.js, LINES 252-256
ISSUE: totalMessagesSent query sums ALL-TIME messages_sent but only for users active in last 7 days. The field name and UI label imply a community-wide total.
FIX: Remove the 7-day filter from the messages query:
pool.query(
`SELECT COALESCE(SUM(messages_sent), 0)::int AS total
FROM user_stats
WHERE guild_id = $1`,
[guildId],
),
2. FILE: web/src/app/community/[guildId]/page.tsx, LINES 54-66
FILE: web/src/app/community/[guildId]/[userId]/page.tsx, LINES 54-67
ISSUE: Duplicated getApiBase() / API_BASE instead of using shared getBotApiBaseUrl() from web/src/lib/bot-api.ts.
FIX: In both files, replace the local API_BASE constant and getApiBase() function with:
import { getBotApiBaseUrl } from '@/lib/bot-api';
const API_BASE = getBotApiBaseUrl() ?? 'http://localhost:3001/api/v1';
Then use API_BASE directly in fetch calls (it already ends with /api/v1).
3. FILE: AGENTS.md
ISSUE: Key Files table is missing new files added in this PR.
FIX: Add these rows to the Key Files table:
- `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
- `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
- `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
- `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index
After changes, run: npx biome check --write && pnpm test
Summary
Public-facing community page (no login required) showing server highlights.
Backend (
src/api/routes/community.js)/community/:guildId/leaderboard— Top 25 members by XP (public profiles only)/community/:guildId/showcases— Project showcase gallery with upvotes/community/:guildId/stats— Community stats (members, messages, projects, challenges)/community/:guildId/profile/:userId— Public user profile with stats + badgesFrontend
/community/[guildId]— SSR community page with stats banner, XP leaderboard, showcase gallery/community/[guildId]/[userId]— SSR public profile with avatar, level, XP bar, stats, badges, projectsMigration
013_public_profiles.cjs— addspublic_profileboolean touser_statswith partial indexTests
Closes #36