feat: user profile customization with banner, accent color, bio, and full profile modal

Add banner image, accent color, and bio fields to user profiles with
full-stack support: schema migration, API validation (hex color format,
190-char bio limit), sanitizeUser propagation, and new GET /users/:id/mutuals
endpoint. Rewrite AccountPanel with live preview card, avatar/banner upload
via ImageCropModal, 16-preset accent color picker, and bio editor. Enhance
UserProfilePopout with banner display, accent-colored names, bio rendering,
and mutual counts. Add new UserProfileModal with About/Mutual Friends/Mutual
Spaces tabs and friend action buttons.
This commit is contained in:
Jannis Braun
2026-03-10 17:08:29 +01:00
parent 88bfc17580
commit 003eff2268
16 changed files with 1066 additions and 72 deletions
+4 -1
View File
@@ -14,11 +14,14 @@ export function resolveAssetUrl(filename: string | null | undefined, origin: str
* Rewrite the avatar field on a user-like object for remote origins.
* Mutates in-place for efficiency (called on arrays of members/messages).
*/
export function normalizeUserAssets<T extends { avatar?: string | null }>(user: T, origin: string): T {
export function normalizeUserAssets<T extends { avatar?: string | null; banner?: string | null }>(user: T, origin: string): T {
if (!origin) return user;
if (user.avatar) {
user.avatar = resolveAssetUrl(user.avatar, origin) ?? user.avatar;
}
if (user.banner) {
user.banner = resolveAssetUrl(user.banner, origin) ?? user.banner;
}
// Qualify users local to the remote instance with their origin domain.
// These users have no homeInstance (they're native there), so the client
// can't distinguish them from its own local users without this step.
+8
View File
@@ -56,3 +56,11 @@ export function getSpaceGradient(id?: string | null, name?: string): GradientEnt
const key = id || name || 'unknown';
return SPACE_GRADIENTS[hashString(key) % SPACE_GRADIENTS.length]!;
}
/** Shift each RGB component of a hex color by `amount` (positive = lighter, negative = darker). */
export function adjustColor(hex: string, amount: number): string {
const r = Math.max(0, Math.min(255, parseInt(hex.slice(1, 3), 16) + amount));
const g = Math.max(0, Math.min(255, parseInt(hex.slice(3, 5), 16) + amount));
const b = Math.max(0, Math.min(255, parseInt(hex.slice(5, 7), 16) + amount));
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}