generated from nhcarrigan/template
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>
This commit was merged in pull request #58.
This commit is contained in:
@@ -3,9 +3,11 @@
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
export * from "./lib/achievement.constants";
|
||||
export * from "./lib/achievement.types";
|
||||
export type * from "./lib/art.types";
|
||||
export * from "./lib/audit.types";
|
||||
export type * from "./lib/auth.types";
|
||||
export * from "./lib/auth.types";
|
||||
export * from "./lib/book.types";
|
||||
export type * from "./lib/comment.types";
|
||||
export type * from "./lib/common.types";
|
||||
@@ -13,5 +15,6 @@ export * from "./lib/game.types";
|
||||
export type * from "./lib/like.types";
|
||||
export * from "./lib/manga.types";
|
||||
export * from "./lib/music.types";
|
||||
export * from "./lib/report.types";
|
||||
export * from "./lib/show.types";
|
||||
export * from "./lib/suggestion.types";
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
import {
|
||||
AchievementCategory,
|
||||
AchievementDefinition,
|
||||
AchievementTier,
|
||||
} from "./achievement.types";
|
||||
|
||||
export const ACHIEVEMENTS: Record<string, AchievementDefinition> = {
|
||||
// ========== SUGGESTION ACHIEVEMENTS (15) ==========
|
||||
suggestion_first_steps: {
|
||||
key: "suggestion_first_steps",
|
||||
title: "First Steps",
|
||||
description: "Submit your first 10 suggestions to the library",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "🌱",
|
||||
points: 50,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
suggestion_contributor: {
|
||||
key: "suggestion_contributor",
|
||||
title: "Contributor",
|
||||
description: "Submit 50 suggestions to the library",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📝",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
suggestion_dedicated: {
|
||||
key: "suggestion_dedicated",
|
||||
title: "Dedicated",
|
||||
description: "Submit 100 suggestions to the library",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "⭐",
|
||||
points: 250,
|
||||
requirements: { count: 100 },
|
||||
},
|
||||
suggestion_master: {
|
||||
key: "suggestion_master",
|
||||
title: "Master Curator",
|
||||
description: "Submit 250 suggestions to the library",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "💎",
|
||||
points: 500,
|
||||
requirements: { count: 250 },
|
||||
},
|
||||
suggestion_legend: {
|
||||
key: "suggestion_legend",
|
||||
title: "Legend",
|
||||
description: "Submit 500 suggestions to the library",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "👑",
|
||||
points: 1000,
|
||||
requirements: { count: 500 },
|
||||
},
|
||||
suggestion_approved: {
|
||||
key: "suggestion_approved",
|
||||
title: "Approved!",
|
||||
description: "Have your first suggestion accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "✅",
|
||||
points: 50,
|
||||
requirements: { count: 1 },
|
||||
},
|
||||
suggestion_quality_5: {
|
||||
key: "suggestion_quality_5",
|
||||
title: "Quality Curator",
|
||||
description: "Have 5 suggestions accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🌟",
|
||||
points: 100,
|
||||
requirements: { count: 5 },
|
||||
},
|
||||
suggestion_quality_10: {
|
||||
key: "suggestion_quality_10",
|
||||
title: "Elite Curator",
|
||||
description: "Have 10 suggestions accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🏆",
|
||||
points: 200,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
suggestion_quality_25: {
|
||||
key: "suggestion_quality_25",
|
||||
title: "Master Curator",
|
||||
description: "Have 25 suggestions accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "💫",
|
||||
points: 400,
|
||||
requirements: { count: 25 },
|
||||
},
|
||||
suggestion_quality_50: {
|
||||
key: "suggestion_quality_50",
|
||||
title: "Grand Master",
|
||||
description: "Have 50 suggestions accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "🎖️",
|
||||
points: 700,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
suggestion_quality_100: {
|
||||
key: "suggestion_quality_100",
|
||||
title: "Ultimate Curator",
|
||||
description: "Have 100 suggestions accepted",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "👸",
|
||||
points: 1500,
|
||||
requirements: { count: 100 },
|
||||
},
|
||||
suggestion_acceptance_75: {
|
||||
key: "suggestion_acceptance_75",
|
||||
title: "Quality Over Quantity",
|
||||
description: "Maintain a 75%+ acceptance rate (minimum 20 suggestions)",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎯",
|
||||
points: 300,
|
||||
requirements: { rate: 0.75, count: 20 },
|
||||
},
|
||||
suggestion_acceptance_100: {
|
||||
key: "suggestion_acceptance_100",
|
||||
title: "Perfect Record",
|
||||
description: "Achieve 100% acceptance rate (minimum 10 suggestions)",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "✨",
|
||||
points: 500,
|
||||
requirements: { rate: 1.0, count: 10 },
|
||||
},
|
||||
suggestion_renaissance: {
|
||||
key: "suggestion_renaissance",
|
||||
title: "Renaissance Person",
|
||||
description: "Submit accepted suggestions across all 6 media types",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🌈",
|
||||
points: 400,
|
||||
requirements: { diversity: true },
|
||||
},
|
||||
suggestion_enthusiast: {
|
||||
key: "suggestion_enthusiast",
|
||||
title: "Suggestion Enthusiast",
|
||||
description: "Submit 5 suggestions in one day",
|
||||
category: AchievementCategory.Suggestion,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🔥",
|
||||
points: 150,
|
||||
requirements: { count: 5, dayRange: 1 },
|
||||
},
|
||||
|
||||
// ========== LIKE ACHIEVEMENTS (12) ==========
|
||||
like_first: {
|
||||
key: "like_first",
|
||||
title: "First Like",
|
||||
description: "Like your first item in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "❤️",
|
||||
points: 25,
|
||||
requirements: { count: 1 },
|
||||
},
|
||||
like_enthusiast: {
|
||||
key: "like_enthusiast",
|
||||
title: "Enthusiast",
|
||||
description: "Like 25 items in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "💕",
|
||||
points: 50,
|
||||
requirements: { count: 25 },
|
||||
},
|
||||
like_fan: {
|
||||
key: "like_fan",
|
||||
title: "Fan",
|
||||
description: "Like 100 items in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "💖",
|
||||
points: 100,
|
||||
requirements: { count: 100 },
|
||||
},
|
||||
like_super_fan: {
|
||||
key: "like_super_fan",
|
||||
title: "Super Fan",
|
||||
description: "Like 250 items in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "💝",
|
||||
points: 250,
|
||||
requirements: { count: 250 },
|
||||
},
|
||||
like_mega_fan: {
|
||||
key: "like_mega_fan",
|
||||
title: "Mega Fan",
|
||||
description: "Like 500 items in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "💗",
|
||||
points: 500,
|
||||
requirements: { count: 500 },
|
||||
},
|
||||
like_legendary: {
|
||||
key: "like_legendary",
|
||||
title: "Legendary Fan",
|
||||
description: "Like 1000 items in the library",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "💞",
|
||||
points: 1000,
|
||||
requirements: { count: 1000 },
|
||||
},
|
||||
like_book_lover: {
|
||||
key: "like_book_lover",
|
||||
title: "Book Lover",
|
||||
description: "Like 50 books",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📚",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
like_gamer: {
|
||||
key: "like_gamer",
|
||||
title: "Gamer",
|
||||
description: "Like 50 games",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🎮",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
like_cinephile: {
|
||||
key: "like_cinephile",
|
||||
title: "Cinephile",
|
||||
description: "Like 50 shows/films",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🎬",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
like_music: {
|
||||
key: "like_music",
|
||||
title: "Music Enthusiast",
|
||||
description: "Like 50 music albums",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🎵",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
like_diverse: {
|
||||
key: "like_diverse",
|
||||
title: "Diverse Taste",
|
||||
description: "Like items from all 6 media types",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🌍",
|
||||
points: 200,
|
||||
requirements: { diversity: true },
|
||||
},
|
||||
like_binge: {
|
||||
key: "like_binge",
|
||||
title: "Binge Liker",
|
||||
description: "Like 20+ items in one day",
|
||||
category: AchievementCategory.Like,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "⚡",
|
||||
points: 150,
|
||||
requirements: { count: 20, dayRange: 1 },
|
||||
},
|
||||
|
||||
// ========== COMMENT ACHIEVEMENTS (12) ==========
|
||||
comment_first: {
|
||||
key: "comment_first",
|
||||
title: "First Thoughts",
|
||||
description: "Leave your first comment in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "💬",
|
||||
points: 25,
|
||||
requirements: { count: 1 },
|
||||
},
|
||||
comment_reviewer: {
|
||||
key: "comment_reviewer",
|
||||
title: "Reviewer",
|
||||
description: "Leave 10 comments in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "✍️",
|
||||
points: 50,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
comment_critic: {
|
||||
key: "comment_critic",
|
||||
title: "Critic",
|
||||
description: "Leave 50 comments in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🗣️",
|
||||
points: 100,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
comment_expert: {
|
||||
key: "comment_expert",
|
||||
title: "Expert Critic",
|
||||
description: "Leave 100 comments in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎭",
|
||||
points: 250,
|
||||
requirements: { count: 100 },
|
||||
},
|
||||
comment_master: {
|
||||
key: "comment_master",
|
||||
title: "Master Reviewer",
|
||||
description: "Leave 250 comments in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "📖",
|
||||
points: 500,
|
||||
requirements: { count: 250 },
|
||||
},
|
||||
comment_legend: {
|
||||
key: "comment_legend",
|
||||
title: "Review Legend",
|
||||
description: "Leave 500 comments in the library",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "🏅",
|
||||
points: 1000,
|
||||
requirements: { count: 500 },
|
||||
},
|
||||
comment_detailed: {
|
||||
key: "comment_detailed",
|
||||
title: "Detailed Critic",
|
||||
description: "Write 10 comments with 500+ characters",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📝",
|
||||
points: 150,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
comment_essay: {
|
||||
key: "comment_essay",
|
||||
title: "Essay Writer",
|
||||
description: "Write 5 comments with 1000+ characters",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "📄",
|
||||
points: 300,
|
||||
requirements: { count: 5 },
|
||||
},
|
||||
comment_novel: {
|
||||
key: "comment_novel",
|
||||
title: "Novel Writer",
|
||||
description: "Write 3 comments with 2000+ characters",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "📚",
|
||||
points: 500,
|
||||
requirements: { count: 3 },
|
||||
},
|
||||
comment_thoughtful: {
|
||||
key: "comment_thoughtful",
|
||||
title: "Thoughtful Reviewer",
|
||||
description: "Comment on 50 different items",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "💭",
|
||||
points: 150,
|
||||
requirements: { uniqueItems: 50 },
|
||||
},
|
||||
comment_diverse: {
|
||||
key: "comment_diverse",
|
||||
title: "Well-Rounded Critic",
|
||||
description: "Comment on all 6 media types",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎨",
|
||||
points: 250,
|
||||
requirements: { diversity: true },
|
||||
},
|
||||
comment_first_to_comment: {
|
||||
key: "comment_first_to_comment",
|
||||
title: "Discussion Starter",
|
||||
description: "Be the first to comment on 10 items",
|
||||
category: AchievementCategory.Comment,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🗨️",
|
||||
points: 200,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
|
||||
// ========== ENGAGEMENT ACHIEVEMENTS (15) ==========
|
||||
engagement_welcome: {
|
||||
key: "engagement_welcome",
|
||||
title: "Welcome!",
|
||||
description: "Complete your profile setup",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "👋",
|
||||
points: 25,
|
||||
requirements: {},
|
||||
},
|
||||
engagement_streak_7: {
|
||||
key: "engagement_streak_7",
|
||||
title: "Daily Visitor",
|
||||
description: "Login for 7 days in a row",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "🔥",
|
||||
points: 100,
|
||||
requirements: { streak: 7 },
|
||||
},
|
||||
engagement_streak_30: {
|
||||
key: "engagement_streak_30",
|
||||
title: "Dedicated",
|
||||
description: "Login for 30 days in a row",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "💪",
|
||||
points: 250,
|
||||
requirements: { streak: 30 },
|
||||
},
|
||||
engagement_streak_100: {
|
||||
key: "engagement_streak_100",
|
||||
title: "Committed",
|
||||
description: "Login for 100 days in a row",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🏆",
|
||||
points: 500,
|
||||
requirements: { streak: 100 },
|
||||
},
|
||||
engagement_streak_365: {
|
||||
key: "engagement_streak_365",
|
||||
title: "Unstoppable",
|
||||
description: "Login for 365 days in a row",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "⚡",
|
||||
points: 2000,
|
||||
requirements: { streak: 365 },
|
||||
},
|
||||
engagement_triple_threat: {
|
||||
key: "engagement_triple_threat",
|
||||
title: "Triple Threat",
|
||||
description: "Submit a suggestion, like an item, and leave a comment in the same day",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🎯",
|
||||
points: 200,
|
||||
requirements: { dayRange: 1 },
|
||||
},
|
||||
engagement_power_user: {
|
||||
key: "engagement_power_user",
|
||||
title: "Power User",
|
||||
description: "Achieve Triple Threat 10 times",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "💫",
|
||||
points: 750,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
engagement_early_adopter: {
|
||||
key: "engagement_early_adopter",
|
||||
title: "Early Adopter",
|
||||
description: "Be among the first 10 users to join",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "🌟",
|
||||
points: 1000,
|
||||
requirements: {},
|
||||
},
|
||||
engagement_founding_100: {
|
||||
key: "engagement_founding_100",
|
||||
title: "Founding Member",
|
||||
description: "Be among the first 100 users to join",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎖️",
|
||||
points: 500,
|
||||
requirements: {},
|
||||
},
|
||||
engagement_founding_1000: {
|
||||
key: "engagement_founding_1000",
|
||||
title: "Pioneer",
|
||||
description: "Be among the first 1000 users to join",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🚀",
|
||||
points: 200,
|
||||
requirements: {},
|
||||
},
|
||||
engagement_veteran_30: {
|
||||
key: "engagement_veteran_30",
|
||||
title: "Veteran",
|
||||
description: "Have an account for 30 days",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "⏰",
|
||||
points: 50,
|
||||
requirements: { dayRange: 30 },
|
||||
},
|
||||
engagement_veteran_180: {
|
||||
key: "engagement_veteran_180",
|
||||
title: "Seasoned",
|
||||
description: "Have an account for 6 months",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📅",
|
||||
points: 150,
|
||||
requirements: { dayRange: 180 },
|
||||
},
|
||||
engagement_veteran_365: {
|
||||
key: "engagement_veteran_365",
|
||||
title: "Longstanding",
|
||||
description: "Have an account for 1 year",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎂",
|
||||
points: 300,
|
||||
requirements: { dayRange: 365 },
|
||||
},
|
||||
engagement_veteran_730: {
|
||||
key: "engagement_veteran_730",
|
||||
title: "Elder",
|
||||
description: "Have an account for 2 years",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "🗓️",
|
||||
points: 600,
|
||||
requirements: { dayRange: 730 },
|
||||
},
|
||||
engagement_veteran_1825: {
|
||||
key: "engagement_veteran_1825",
|
||||
title: "Legend",
|
||||
description: "Have an account for 5 years",
|
||||
category: AchievementCategory.Engagement,
|
||||
tier: AchievementTier.Diamond,
|
||||
icon: "🌠",
|
||||
points: 1500,
|
||||
requirements: { dayRange: 1825 },
|
||||
},
|
||||
|
||||
// ========== REPORT ACHIEVEMENTS (8) ==========
|
||||
report_watchful: {
|
||||
key: "report_watchful",
|
||||
title: "Watchful Eye",
|
||||
description: "Submit your first valid report (ACTION_TAKEN)",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Bronze,
|
||||
icon: "👀",
|
||||
points: 50,
|
||||
requirements: { count: 1 },
|
||||
},
|
||||
report_guardian: {
|
||||
key: "report_guardian",
|
||||
title: "Guardian",
|
||||
description: "Have 5 reports result in ACTION_TAKEN",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "🛡️",
|
||||
points: 100,
|
||||
requirements: { count: 5 },
|
||||
},
|
||||
report_protector: {
|
||||
key: "report_protector",
|
||||
title: "Protector",
|
||||
description: "Have 10 reports result in ACTION_TAKEN",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "⚔️",
|
||||
points: 250,
|
||||
requirements: { count: 10 },
|
||||
},
|
||||
report_vigilant: {
|
||||
key: "report_vigilant",
|
||||
title: "Vigilant Protector",
|
||||
description: "Have 25 reports result in ACTION_TAKEN",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "🔰",
|
||||
points: 500,
|
||||
requirements: { count: 25 },
|
||||
},
|
||||
report_accuracy_80: {
|
||||
key: "report_accuracy_80",
|
||||
title: "Sharp Eye",
|
||||
description: "Maintain 80%+ ACTION_TAKEN rate (minimum 10 reports)",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Gold,
|
||||
icon: "🎯",
|
||||
points: 300,
|
||||
requirements: { rate: 0.8, count: 10 },
|
||||
},
|
||||
report_accuracy_90: {
|
||||
key: "report_accuracy_90",
|
||||
title: "Eagle Eye",
|
||||
description: "Maintain 90%+ ACTION_TAKEN rate (minimum 10 reports)",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Platinum,
|
||||
icon: "🦅",
|
||||
points: 500,
|
||||
requirements: { rate: 0.9, count: 10 },
|
||||
},
|
||||
report_consistent: {
|
||||
key: "report_consistent",
|
||||
title: "Consistent Guardian",
|
||||
description: "Submit at least one report weekly for 4 weeks",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📆",
|
||||
points: 200,
|
||||
requirements: { count: 4 },
|
||||
},
|
||||
report_volume: {
|
||||
key: "report_volume",
|
||||
title: "Dedicated Reporter",
|
||||
description: "Submit 50 reports (any status)",
|
||||
category: AchievementCategory.Report,
|
||||
tier: AchievementTier.Silver,
|
||||
icon: "📝",
|
||||
points: 150,
|
||||
requirements: { count: 50 },
|
||||
},
|
||||
};
|
||||
|
||||
export const ACHIEVEMENT_LIST = Object.values(ACHIEVEMENTS);
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @copyright NHCarrigan
|
||||
* @license Naomi's Public License
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
export enum AchievementCategory {
|
||||
Suggestion = "SUGGESTION",
|
||||
Like = "LIKE",
|
||||
Comment = "COMMENT",
|
||||
Engagement = "ENGAGEMENT",
|
||||
Report = "REPORT",
|
||||
}
|
||||
|
||||
export enum AchievementTier {
|
||||
Bronze = "BRONZE",
|
||||
Silver = "SILVER",
|
||||
Gold = "GOLD",
|
||||
Platinum = "PLATINUM",
|
||||
Diamond = "DIAMOND",
|
||||
}
|
||||
|
||||
export interface AchievementRequirements {
|
||||
count?: number;
|
||||
rate?: number;
|
||||
streak?: number;
|
||||
diversity?: boolean;
|
||||
uniqueItems?: number;
|
||||
dayRange?: number;
|
||||
}
|
||||
|
||||
export interface AchievementDefinition {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
category: AchievementCategory;
|
||||
tier: AchievementTier;
|
||||
icon: string;
|
||||
points: number;
|
||||
requirements: AchievementRequirements;
|
||||
}
|
||||
|
||||
export interface UserAchievement {
|
||||
id: string;
|
||||
userId: string;
|
||||
achievementKey: string;
|
||||
progress: number;
|
||||
earned: boolean;
|
||||
earnedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AchievementProgress {
|
||||
definition: AchievementDefinition;
|
||||
progress: number;
|
||||
earned: boolean;
|
||||
earnedAt?: Date;
|
||||
}
|
||||
|
||||
export interface UserAchievementSummary {
|
||||
totalPoints: number;
|
||||
totalEarned: number;
|
||||
recentAchievements: AchievementProgress[];
|
||||
progressByCategory: {
|
||||
category: AchievementCategory;
|
||||
earned: number;
|
||||
total: number;
|
||||
}[];
|
||||
}
|
||||
@@ -21,6 +21,7 @@ enum AuditAction {
|
||||
rateLimitExceeded = "RATE_LIMIT_EXCEEDED",
|
||||
csrfValidationFailed = "CSRF_VALIDATION_FAILED",
|
||||
unauthorizedAccess = "UNAUTHORIZED_ACCESS",
|
||||
achievementUnlocked = "ACHIEVEMENT_UNLOCKED",
|
||||
}
|
||||
|
||||
enum AuditCategory {
|
||||
|
||||
@@ -4,18 +4,39 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention -- Prisma enum values use UPPER_CASE */
|
||||
enum PrimaryBadge {
|
||||
STAFF = "STAFF",
|
||||
MOD = "MOD",
|
||||
VIP = "VIP",
|
||||
DISCORD = "DISCORD",
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
discordId: string;
|
||||
isAdmin: boolean;
|
||||
isBanned: boolean;
|
||||
inDiscord: boolean;
|
||||
isVip: boolean;
|
||||
isMod: boolean;
|
||||
isStaff: boolean;
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
slug?: string;
|
||||
displayName?: string;
|
||||
bio?: string;
|
||||
profilePublic: boolean;
|
||||
primaryBadge?: PrimaryBadge;
|
||||
website?: string;
|
||||
discordServer?: string;
|
||||
bluesky?: string;
|
||||
github?: string;
|
||||
linkedin?: string;
|
||||
twitch?: string;
|
||||
youtube?: string;
|
||||
discordId: string;
|
||||
isAdmin: boolean;
|
||||
isBanned: boolean;
|
||||
inDiscord: boolean;
|
||||
isVip: boolean;
|
||||
isMod: boolean;
|
||||
isStaff: boolean;
|
||||
}
|
||||
|
||||
interface JwtPayload {
|
||||
@@ -36,4 +57,5 @@ interface AuthResponse {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export { PrimaryBadge };
|
||||
export type { AuthResponse, JwtPayload, User };
|
||||
|
||||
@@ -4,30 +4,34 @@
|
||||
* @author Naomi Carrigan
|
||||
*/
|
||||
|
||||
import { PrimaryBadge } from "./auth.types";
|
||||
|
||||
interface CommentUser {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
inDiscord?: boolean;
|
||||
isVip?: boolean;
|
||||
isMod?: boolean;
|
||||
isStaff?: boolean;
|
||||
id: string;
|
||||
username: string;
|
||||
avatar?: string;
|
||||
primaryBadge?: PrimaryBadge;
|
||||
inDiscord?: boolean;
|
||||
isVip?: boolean;
|
||||
isMod?: boolean;
|
||||
isStaff?: boolean;
|
||||
}
|
||||
|
||||
interface Comment {
|
||||
id: string;
|
||||
content: string;
|
||||
rawContent?: string;
|
||||
userId: string;
|
||||
user: CommentUser;
|
||||
gameId?: string;
|
||||
bookId?: string;
|
||||
musicId?: string;
|
||||
artId?: string;
|
||||
showId?: string;
|
||||
mangaId?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
id: string;
|
||||
content: string;
|
||||
rawContent?: string;
|
||||
userId: string;
|
||||
user: CommentUser;
|
||||
gameId?: string;
|
||||
bookId?: string;
|
||||
musicId?: string;
|
||||
artId?: string;
|
||||
showId?: string;
|
||||
mangaId?: string;
|
||||
hasPendingReports?: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface CreateCommentDto {
|
||||
|
||||
@@ -32,16 +32,16 @@ interface Game {
|
||||
}
|
||||
|
||||
interface CreateGameDto {
|
||||
title: string;
|
||||
platform?: string;
|
||||
status: GameStatus;
|
||||
title: string;
|
||||
platform?: string;
|
||||
status: GameStatus;
|
||||
dateStarted?: Date;
|
||||
dateFinished?: Date;
|
||||
rating?: number;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
tags?: Array<string>;
|
||||
links?: Array<Link>;
|
||||
rating?: number;
|
||||
notes?: string;
|
||||
coverImage?: string;
|
||||
tags?: Array<string>;
|
||||
links?: Array<Link>;
|
||||
}
|
||||
|
||||
interface UpdateGameDto extends Partial<CreateGameDto> {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
export enum ReportReason {
|
||||
INAPPROPRIATE_CONTENT = "INAPPROPRIATE_CONTENT",
|
||||
HARASSMENT = "HARASSMENT",
|
||||
SPAM = "SPAM",
|
||||
IMPERSONATION = "IMPERSONATION",
|
||||
OFFENSIVE_NAME = "OFFENSIVE_NAME",
|
||||
MALICIOUS_LINKS = "MALICIOUS_LINKS",
|
||||
OTHER = "OTHER",
|
||||
}
|
||||
|
||||
export enum ReportStatus {
|
||||
PENDING = "PENDING",
|
||||
REVIEWED = "REVIEWED",
|
||||
DISMISSED = "DISMISSED",
|
||||
ACTION_TAKEN = "ACTION_TAKEN",
|
||||
}
|
||||
|
||||
export interface ProfileReport {
|
||||
id: string;
|
||||
reportedUserId: string;
|
||||
reporterId: string;
|
||||
reason: ReportReason;
|
||||
details: string;
|
||||
status: ReportStatus;
|
||||
reviewedBy?: string;
|
||||
reviewNotes?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ProfileReportWithUsers extends ProfileReport {
|
||||
reportedUser: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
reporter: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
reviewer?: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateReportDto {
|
||||
reportedUserId: string;
|
||||
reason: ReportReason;
|
||||
details: string;
|
||||
}
|
||||
|
||||
export interface UpdateReportDto {
|
||||
status: ReportStatus;
|
||||
reviewNotes?: string;
|
||||
}
|
||||
|
||||
export interface CommentReport {
|
||||
id: string;
|
||||
reportedCommentId: string;
|
||||
reporterId: string;
|
||||
reason: ReportReason;
|
||||
details: string;
|
||||
status: ReportStatus;
|
||||
reviewedBy?: string;
|
||||
reviewNotes?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface CommentReportWithDetails extends CommentReport {
|
||||
reportedComment: {
|
||||
id: string;
|
||||
content: string;
|
||||
rawContent?: string;
|
||||
userId: string;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
};
|
||||
reporter: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
reviewer?: {
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateCommentReportDto {
|
||||
reportedCommentId: string;
|
||||
reason: ReportReason;
|
||||
details: string;
|
||||
}
|
||||
|
||||
export interface UpdateCommentReportDto {
|
||||
status: ReportStatus;
|
||||
reviewNotes?: string;
|
||||
}
|
||||
@@ -10,30 +10,32 @@ 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,
|
||||
username: "testuser",
|
||||
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,
|
||||
username: "anotheruser",
|
||||
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");
|
||||
@@ -44,16 +46,17 @@ describe("auth Types", () => {
|
||||
|
||||
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,
|
||||
username: "banneduser",
|
||||
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();
|
||||
@@ -97,17 +100,18 @@ describe("auth Types", () => {
|
||||
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,
|
||||
username: "testuser",
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user