Files
naomi 86404497f0
Node.js CI / CI (push) Successful in 1m21s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m22s
feat: implement user profiles with achievements and primary badge system (#58)
## Summary

This PR implements comprehensive user profile enhancements including:
- User profile pages showing stats, badges, social links, and bio
- Achievement system with 62 achievements across 5 categories
- Primary badge selection allowing users to display their preferred badge
- Admin profile editing capabilities

## Changes

### User Profiles (#45)
- **Frontend**: User profile pages with stats display
  - Profile cards showing avatar, display name, username, and bio
  - Social links section (Website, GitHub, Bluesky, LinkedIn, Twitch, YouTube, Discord)
  - Stats display (suggestions, accepted suggestions, likes, comments)
  - Recent achievements section
  - Badge display
  - Report button for other users' profiles
- **Backend**: Profile API endpoints
  - Get user profile by username or ID
  - Profile includes stats, badges, and achievement points

### Achievement System (#48)
- **Database**: UserAchievement model for tracking progress
- **62 Total Achievements** across 5 categories:
  - **Suggestions (15)**: First suggestion through ultimate curator
  - **Likes (12)**: First like through legendary fan
  - **Comments (12)**: First comment through review legend
  - **Engagement (15)**: Login streaks and activity milestones
  - **Reports (8)**: Valid reports and accuracy tracking
- **Backend**: AchievementService with real-time checking
  - Integrated into all user interaction points
  - API endpoints for achievement data
  - Progress tracking to avoid recalculation
- **Frontend**: Achievements page and profile integration
  - Full achievements page with category filtering
  - Tier-based styling (Bronze, Silver, Gold, Platinum, Diamond)
  - Progress indicators for in-progress achievements
  - Recent achievements on profile pages

### Primary Badge System (#49)
- **Database**: Add primaryBadge field to User model
- **Backend**: Update profile endpoints to include primary badge
- **Frontend**: Primary badge selection in settings
  - Only shows badges the user has earned
  - Displayed on profile page
  - Displayed in comments (next to username)
  - Falls back to no badge if selection is invalid
- **Admin Features**: Admin can edit any user's primary badge

### Admin Enhancements
- Comprehensive profile editing modal for admins
  - Edit display name, bio, slug, social links
  - Set primary badge for users
  - Visual feedback for save/error states
- Admin action buttons in report review modals
  - Ban user, delete comment, edit profile
  - Integrated with report workflow

### Quality Improvements
- Improved dropdown option contrast for readability
- Hide all badges when no primary badge is selected
- "View All" achievements link only shown on own profile
- Improved achievement text readability

## Testing

-  User profiles display correctly with stats and badges
-  Achievement checking works for all interaction types
-  Primary badge selection persists and displays correctly
-  Admin profile editing saves successfully
-  Report workflow integrated with admin actions
-  Achievements page shows all 62 achievements with filtering
-  Text readability improved across components

Closes #45
Closes #48
Closes #49

Co-authored-by: Hikari <hikari@nhcarrigan.com>
Reviewed-on: #58
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
2026-02-19 22:21:17 -08:00

123 lines
3.7 KiB
TypeScript

/**
* @copyright NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { AuthResponse, JwtPayload, User } from "../src/lib/auth.types";
describe("auth Types", () => {
describe("user interface", () => {
it("should accept valid user objects", () => {
const userWithAvatar: User = {
avatar: "https://example.com/avatar.png",
discordId: "discord123",
email: "test@example.com",
id: "user123",
inDiscord: true,
isAdmin: true,
isBanned: false,
isMod: true,
isStaff: true,
isVip: false,
profilePublic: true,
username: "testuser",
};
const userWithoutAvatar: User = {
discordId: "discord456",
email: "another@example.com",
id: "user456",
inDiscord: false,
isAdmin: false,
isBanned: false,
isMod: false,
isStaff: false,
isVip: true,
profilePublic: false,
username: "anotheruser",
};
expect(userWithAvatar.avatar).toBe("https://example.com/avatar.png");
expect(userWithoutAvatar.avatar).toBeUndefined();
expect(userWithAvatar.isAdmin).toBeTruthy();
expect(userWithoutAvatar.isVip).toBeTruthy();
});
it("should handle all boolean flags correctly", () => {
const bannedUser: User = {
discordId: "discord789",
email: "banned@example.com",
id: "banned123",
inDiscord: false,
isAdmin: false,
isBanned: true,
isMod: false,
isStaff: false,
isVip: false,
profilePublic: false,
username: "banneduser",
};
expect(bannedUser.isBanned).toBeTruthy();
expect(bannedUser.isAdmin).toBeFalsy();
expect(bannedUser.inDiscord).toBeFalsy();
});
});
describe("jwtPayload interface", () => {
it("should accept JWT payload with required fields", () => {
const payload: JwtPayload = {
email: "test@example.com",
isAdmin: false,
sub: "user123",
username: "testuser",
};
expect(payload.sub).toBe("user123");
expect(payload.iat).toBeUndefined();
expect(payload.exp).toBeUndefined();
});
it("should accept JWT payload with timestamps", () => {
const now = Math.floor(Date.now() / 1000);
const payloadWithTimestamps: JwtPayload = {
email: "another@example.com",
exp: now + 3600,
iat: now,
isAdmin: true,
sub: "user456",
username: "anotheruser", // 1 hour
};
expect(payloadWithTimestamps.iat).toBe(now);
expect(payloadWithTimestamps.exp).toBe(now + 3600);
});
});
describe("authResponse interface", () => {
it("should accept valid auth response", () => {
const authResponse: AuthResponse = {
accessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
user: {
avatar: "https://example.com/avatar.png",
discordId: "discord123",
email: "test@example.com",
id: "user123",
inDiscord: true,
isAdmin: false,
isBanned: false,
isMod: false,
isStaff: false,
isVip: false,
profilePublic: true,
username: "testuser",
},
};
expect(authResponse.accessToken).toContain("eyJ");
expect(authResponse.user.username).toBe("testuser");
});
});
});