generated from nhcarrigan/template
feat: implement user profiles and settings
Add comprehensive user profile system allowing users to showcase their activity and customize their profiles. Database Changes: - Added profile fields to User model: slug, displayName, bio, profilePublic - Added index on slug field for efficient lookups API Changes: - Added GET /users/me endpoint to fetch current user - Added PUT /users/me endpoint to update user settings - Added GET /users/profile/:identifier endpoint for public profiles - Updated UserService with profile methods and statistics - Modified AuthService to include profile fields in user responses Frontend Changes: - Created ProfileComponent to display user profiles with stats - Created SettingsComponent for profile customization - Added profile and settings routes - Updated header dropdown menu with profile links - Enhanced UserService with profile methods - Added updateUser method to AuthService Features: - Custom profile slugs for clean URLs - Display names separate from usernames - User bios (up to 500 characters) - Public/private profile toggle - Activity statistics (suggestions, likes, comments, acceptance rate) - Badge display (Staff, Mod, VIP, Discord Member) - Beautiful witch-themed styling Closes #45
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import { User } from "@library/shared-types";
|
||||
import { prisma } from "../lib/prisma";
|
||||
import { SuggestionStatus } from "@prisma/client";
|
||||
|
||||
export class UserService {
|
||||
private prisma = prisma;
|
||||
@@ -21,6 +22,10 @@ export class UserService {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
@@ -45,6 +50,10 @@ export class UserService {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
@@ -66,6 +75,10 @@ export class UserService {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
@@ -87,6 +100,10 @@ export class UserService {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
@@ -104,4 +121,137 @@ export class UserService {
|
||||
|
||||
return user?.isBanned ?? false;
|
||||
}
|
||||
|
||||
async getUserBySlug(slug: string): Promise<User | null> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
discordId: user.discordId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
isVip: user.isVip,
|
||||
isMod: user.isMod,
|
||||
isStaff: user.isStaff,
|
||||
};
|
||||
}
|
||||
|
||||
async updateUserSettings(
|
||||
id: string,
|
||||
updates: {
|
||||
slug?: string;
|
||||
displayName?: string;
|
||||
bio?: string;
|
||||
profilePublic?: boolean;
|
||||
}
|
||||
): Promise<User | null> {
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: updates,
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
discordId: user.discordId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar || undefined,
|
||||
slug: user.slug || undefined,
|
||||
displayName: user.displayName || undefined,
|
||||
bio: user.bio || undefined,
|
||||
profilePublic: user.profilePublic,
|
||||
isAdmin: user.isAdmin,
|
||||
isBanned: user.isBanned,
|
||||
inDiscord: user.inDiscord,
|
||||
isVip: user.isVip,
|
||||
isMod: user.isMod,
|
||||
isStaff: user.isStaff,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserProfile(identifier: string): Promise<{
|
||||
id: string;
|
||||
username: string;
|
||||
displayName?: string | null;
|
||||
avatar?: string | null;
|
||||
bio?: string | null;
|
||||
slug?: string | null;
|
||||
isStaff: boolean;
|
||||
isMod: boolean;
|
||||
isVip: boolean;
|
||||
inDiscord: boolean;
|
||||
profilePublic: boolean;
|
||||
createdAt: Date;
|
||||
stats: {
|
||||
suggestionsCount: number;
|
||||
suggestionsAcceptedCount: number;
|
||||
likesCount: number;
|
||||
commentsCount: number;
|
||||
};
|
||||
} | null> {
|
||||
// Try to find by slug first, then by id if it's a valid ObjectId
|
||||
const isValidObjectId = /^[0-9a-f]{24}$/i.test(identifier);
|
||||
|
||||
const whereConditions = isValidObjectId
|
||||
? [{ slug: identifier }, { id: identifier }]
|
||||
: [{ slug: identifier }];
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: whereConditions,
|
||||
},
|
||||
include: {
|
||||
suggestions: {
|
||||
select: { id: true, status: true },
|
||||
},
|
||||
likes: {
|
||||
select: { id: true },
|
||||
},
|
||||
comments: {
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.displayName,
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
slug: user.slug,
|
||||
isStaff: user.isStaff,
|
||||
isMod: user.isMod,
|
||||
isVip: user.isVip,
|
||||
inDiscord: user.inDiscord,
|
||||
profilePublic: user.profilePublic,
|
||||
createdAt: user.createdAt,
|
||||
stats: {
|
||||
suggestionsCount: user.suggestions.length,
|
||||
suggestionsAcceptedCount: user.suggestions.filter(
|
||||
(suggestion) => suggestion.status === SuggestionStatus.ACCEPTED
|
||||
).length,
|
||||
likesCount: user.likes.length,
|
||||
commentsCount: user.comments.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user