generated from nhcarrigan/template
feat: add social links with validation and Font Awesome icons
Added comprehensive social links functionality to user profiles: **New Social Platforms:** - Website (full URL validation) - GitHub (username format) - Bluesky (handle format) - LinkedIn (username format) - Twitch (username format) - YouTube (handle or channel ID format) - Discord Server (invite code format) **Features:** - Database schema updated with 7 new optional social link fields - Backend services and API routes updated to handle all social links - Settings form with input fields and helpful validation hints - Profile display with Font Awesome icons for each platform - Regex validation patterns for all fields with visual feedback - Green border for valid input, red border for invalid input - All form inputs use consistent type="text" for uniform styling - Discord accepts just invite code (constructs full URL automatically) **Technical Changes:** - Installed @fortawesome/angular-fontawesome with pinned versions - Replaced emoji icons with proper Font Awesome components - Added FontAwesomeModule to profile component - Updated all User type interfaces across frontend and backend - Updated UserService mappings in all methods - Added comprehensive regex patterns matching platform requirements Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,13 @@ model User {
|
|||||||
displayName String?
|
displayName String?
|
||||||
bio String?
|
bio String?
|
||||||
profilePublic Boolean @default(true)
|
profilePublic Boolean @default(true)
|
||||||
|
website String?
|
||||||
|
discordServer String?
|
||||||
|
bluesky String?
|
||||||
|
github String?
|
||||||
|
linkedin String?
|
||||||
|
twitch String?
|
||||||
|
youtube String?
|
||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
isBanned Boolean @default(false)
|
isBanned Boolean @default(false)
|
||||||
inDiscord Boolean @default(false)
|
inDiscord Boolean @default(false)
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ interface UpdateUserSettingsBody {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserProfileResponse {
|
interface UserProfileResponse {
|
||||||
@@ -24,6 +31,13 @@ interface UserProfileResponse {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
badges: {
|
badges: {
|
||||||
isStaff: boolean;
|
isStaff: boolean;
|
||||||
isMod: boolean;
|
isMod: boolean;
|
||||||
@@ -130,6 +144,13 @@ const usersRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
avatar: profile.avatar,
|
avatar: profile.avatar,
|
||||||
bio: profile.bio,
|
bio: profile.bio,
|
||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
|
website: profile.website,
|
||||||
|
discordServer: profile.discordServer,
|
||||||
|
bluesky: profile.bluesky,
|
||||||
|
github: profile.github,
|
||||||
|
linkedin: profile.linkedin,
|
||||||
|
twitch: profile.twitch,
|
||||||
|
youtube: profile.youtube,
|
||||||
badges: {
|
badges: {
|
||||||
isStaff: profile.isStaff,
|
isStaff: profile.isStaff,
|
||||||
isMod: profile.isMod,
|
isMod: profile.isMod,
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ export class AuthService {
|
|||||||
displayName: dbUser.displayName || undefined,
|
displayName: dbUser.displayName || undefined,
|
||||||
bio: dbUser.bio || undefined,
|
bio: dbUser.bio || undefined,
|
||||||
profilePublic: dbUser.profilePublic,
|
profilePublic: dbUser.profilePublic,
|
||||||
|
website: dbUser.website || undefined,
|
||||||
|
discordServer: dbUser.discordServer || undefined,
|
||||||
|
bluesky: dbUser.bluesky || undefined,
|
||||||
|
github: dbUser.github || undefined,
|
||||||
|
linkedin: dbUser.linkedin || undefined,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isBanned: dbUser.isBanned,
|
isBanned: dbUser.isBanned,
|
||||||
inDiscord: dbUser.inDiscord,
|
inDiscord: dbUser.inDiscord,
|
||||||
@@ -175,6 +180,11 @@ export class AuthService {
|
|||||||
displayName: dbUser.displayName || undefined,
|
displayName: dbUser.displayName || undefined,
|
||||||
bio: dbUser.bio || undefined,
|
bio: dbUser.bio || undefined,
|
||||||
profilePublic: dbUser.profilePublic,
|
profilePublic: dbUser.profilePublic,
|
||||||
|
website: dbUser.website || undefined,
|
||||||
|
discordServer: dbUser.discordServer || undefined,
|
||||||
|
bluesky: dbUser.bluesky || undefined,
|
||||||
|
github: dbUser.github || undefined,
|
||||||
|
linkedin: dbUser.linkedin || undefined,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isBanned: dbUser.isBanned,
|
isBanned: dbUser.isBanned,
|
||||||
inDiscord: dbUser.inDiscord,
|
inDiscord: dbUser.inDiscord,
|
||||||
@@ -229,6 +239,11 @@ export class AuthService {
|
|||||||
displayName: dbUser.displayName || undefined,
|
displayName: dbUser.displayName || undefined,
|
||||||
bio: dbUser.bio || undefined,
|
bio: dbUser.bio || undefined,
|
||||||
profilePublic: dbUser.profilePublic,
|
profilePublic: dbUser.profilePublic,
|
||||||
|
website: dbUser.website || undefined,
|
||||||
|
discordServer: dbUser.discordServer || undefined,
|
||||||
|
bluesky: dbUser.bluesky || undefined,
|
||||||
|
github: dbUser.github || undefined,
|
||||||
|
linkedin: dbUser.linkedin || undefined,
|
||||||
isAdmin: dbUser.isAdmin,
|
isAdmin: dbUser.isAdmin,
|
||||||
isBanned: dbUser.isBanned,
|
isBanned: dbUser.isBanned,
|
||||||
inDiscord: dbUser.inDiscord,
|
inDiscord: dbUser.inDiscord,
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -54,6 +61,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -79,6 +93,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -104,6 +125,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -141,6 +169,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -157,6 +192,13 @@ export class UserService {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
}
|
}
|
||||||
): Promise<User | null> {
|
): Promise<User | null> {
|
||||||
const user = await this.prisma.user.update({
|
const user = await this.prisma.user.update({
|
||||||
@@ -174,6 +216,13 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
website: user.website || undefined,
|
||||||
|
discordServer: user.discordServer || undefined,
|
||||||
|
bluesky: user.bluesky || undefined,
|
||||||
|
github: user.github || undefined,
|
||||||
|
linkedin: user.linkedin || undefined,
|
||||||
|
twitch: user.twitch || undefined,
|
||||||
|
youtube: user.youtube || undefined,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
isBanned: user.isBanned,
|
isBanned: user.isBanned,
|
||||||
inDiscord: user.inDiscord,
|
inDiscord: user.inDiscord,
|
||||||
@@ -190,6 +239,13 @@ export class UserService {
|
|||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
website?: string | null;
|
||||||
|
discordServer?: string | null;
|
||||||
|
bluesky?: string | null;
|
||||||
|
github?: string | null;
|
||||||
|
linkedin?: string | null;
|
||||||
|
twitch?: string | null;
|
||||||
|
youtube?: string | null;
|
||||||
isStaff: boolean;
|
isStaff: boolean;
|
||||||
isMod: boolean;
|
isMod: boolean;
|
||||||
isVip: boolean;
|
isVip: boolean;
|
||||||
@@ -238,6 +294,13 @@ export class UserService {
|
|||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
bio: user.bio,
|
bio: user.bio,
|
||||||
slug: user.slug,
|
slug: user.slug,
|
||||||
|
website: user.website,
|
||||||
|
discordServer: user.discordServer,
|
||||||
|
bluesky: user.bluesky,
|
||||||
|
github: user.github,
|
||||||
|
linkedin: user.linkedin,
|
||||||
|
twitch: user.twitch,
|
||||||
|
youtube: user.youtube,
|
||||||
isStaff: user.isStaff,
|
isStaff: user.isStaff,
|
||||||
isMod: user.isMod,
|
isMod: user.isMod,
|
||||||
isVip: user.isVip,
|
isVip: user.isVip,
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
import { Component, inject, signal, OnInit } from '@angular/core';
|
import { Component, inject, signal, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { faGlobe, faCloud } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faGithub, faLinkedin, faTwitch, faYoutube, faDiscord } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { UserService, UserProfileResponse } from '../../services/user.service';
|
import { UserService, UserProfileResponse } from '../../services/user.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile',
|
selector: 'app-profile',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule, FontAwesomeModule],
|
||||||
template: `
|
template: `
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
@@ -62,6 +65,56 @@ import { ToastService } from '../../services/toast.service';
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (profile()!.website || profile()!.github || profile()!.bluesky || profile()!.linkedin || profile()!.twitch || profile()!.youtube || profile()!.discordServer) {
|
||||||
|
<div class="social-links-section">
|
||||||
|
<h2>Social Links</h2>
|
||||||
|
<div class="social-links">
|
||||||
|
@if (profile()!.website) {
|
||||||
|
<a [href]="profile()!.website" target="_blank" rel="noopener noreferrer" class="social-link" title="Website">
|
||||||
|
<fa-icon [icon]="faGlobe" class="icon"></fa-icon>
|
||||||
|
<span class="label">Website</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.github) {
|
||||||
|
<a [href]="'https://github.com/' + profile()!.github" target="_blank" rel="noopener noreferrer" class="social-link" title="GitHub">
|
||||||
|
<fa-icon [icon]="faGithub" class="icon"></fa-icon>
|
||||||
|
<span class="label">GitHub</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.bluesky) {
|
||||||
|
<a [href]="'https://bsky.app/profile/' + profile()!.bluesky" target="_blank" rel="noopener noreferrer" class="social-link" title="Bluesky">
|
||||||
|
<fa-icon [icon]="faCloud" class="icon"></fa-icon>
|
||||||
|
<span class="label">Bluesky</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.linkedin) {
|
||||||
|
<a [href]="'https://linkedin.com/in/' + profile()!.linkedin" target="_blank" rel="noopener noreferrer" class="social-link" title="LinkedIn">
|
||||||
|
<fa-icon [icon]="faLinkedin" class="icon"></fa-icon>
|
||||||
|
<span class="label">LinkedIn</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.twitch) {
|
||||||
|
<a [href]="'https://twitch.tv/' + profile()!.twitch" target="_blank" rel="noopener noreferrer" class="social-link" title="Twitch">
|
||||||
|
<fa-icon [icon]="faTwitch" class="icon"></fa-icon>
|
||||||
|
<span class="label">Twitch</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.youtube) {
|
||||||
|
<a [href]="'https://youtube.com/' + profile()!.youtube" target="_blank" rel="noopener noreferrer" class="social-link" title="YouTube">
|
||||||
|
<fa-icon [icon]="faYoutube" class="icon"></fa-icon>
|
||||||
|
<span class="label">YouTube</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
@if (profile()!.discordServer) {
|
||||||
|
<a [href]="'https://discord.gg/' + profile()!.discordServer" target="_blank" rel="noopener noreferrer" class="social-link" title="Discord Server">
|
||||||
|
<fa-icon [icon]="faDiscord" class="icon"></fa-icon>
|
||||||
|
<span class="label">Discord</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="stats-section">
|
<div class="stats-section">
|
||||||
<h2>Activity Statistics</h2>
|
<h2>Activity Statistics</h2>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
@@ -212,6 +265,55 @@ import { ToastService } from '../../services/toast.service';
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.social-links-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links-section h2 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--accent-colour, #9b59b6);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(155, 89, 182, 0.2);
|
||||||
|
border: 1px solid rgba(155, 89, 182, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-colour, #e0e0e0);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
background: rgba(155, 89, 182, 0.3);
|
||||||
|
border-color: var(--accent-colour, #9b59b6);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link fa-icon {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link fa-icon ::ng-deep svg {
|
||||||
|
width: 1.3rem;
|
||||||
|
height: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link .label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-section {
|
.stats-section {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -272,6 +374,15 @@ export class ProfileComponent implements OnInit {
|
|||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
|
|
||||||
|
// Font Awesome icons
|
||||||
|
faGlobe = faGlobe;
|
||||||
|
faGithub = faGithub;
|
||||||
|
faCloud = faCloud;
|
||||||
|
faLinkedin = faLinkedin;
|
||||||
|
faTwitch = faTwitch;
|
||||||
|
faYoutube = faYoutube;
|
||||||
|
faDiscord = faDiscord;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
const identifier = this.route.snapshot.paramMap.get('identifier');
|
const identifier = this.route.snapshot.paramMap.get('identifier');
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
|
|||||||
@@ -71,6 +71,101 @@ import { User } from '@library/shared-types';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>Social Links</h2>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="website">Website</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="website"
|
||||||
|
name="website"
|
||||||
|
[(ngModel)]="formData.website"
|
||||||
|
placeholder="https://yourwebsite.com"
|
||||||
|
pattern="https?://.+"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Your personal website or portfolio (must start with http:// or https://)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="github">GitHub</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="github"
|
||||||
|
name="github"
|
||||||
|
[(ngModel)]="formData.github"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Just your GitHub username (alphanumeric and hyphens, 1-39 characters)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bluesky">Bluesky</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="bluesky"
|
||||||
|
name="bluesky"
|
||||||
|
[(ngModel)]="formData.bluesky"
|
||||||
|
placeholder="username.bsky.social"
|
||||||
|
pattern="[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Your full Bluesky handle (e.g., username.bsky.social)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="linkedin">LinkedIn</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="linkedin"
|
||||||
|
name="linkedin"
|
||||||
|
[(ngModel)]="formData.linkedin"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9-]{3,100}"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Just your LinkedIn username (alphanumeric and hyphens, 3-100 characters)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="twitch">Twitch</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="twitch"
|
||||||
|
name="twitch"
|
||||||
|
[(ngModel)]="formData.twitch"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9_]{4,25}"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Just your Twitch username (alphanumeric and underscores, 4-25 characters)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="youtube">YouTube</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="youtube"
|
||||||
|
name="youtube"
|
||||||
|
[(ngModel)]="formData.youtube"
|
||||||
|
placeholder="@username or channel-id"
|
||||||
|
pattern="(@[a-zA-Z0-9_.-]{3,30}|UC[a-zA-Z0-9_-]{22})"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Your YouTube handle (@username) or channel ID (UC...)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="discordServer">Discord Server</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="discordServer"
|
||||||
|
name="discordServer"
|
||||||
|
[(ngModel)]="formData.discordServer"
|
||||||
|
placeholder="invite-code"
|
||||||
|
pattern="[a-zA-Z0-9]{2,32}"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Just your Discord server invite code (alphanumeric, 2-32 characters)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2>Privacy</h2>
|
<h2>Privacy</h2>
|
||||||
|
|
||||||
@@ -187,6 +282,16 @@ import { User } from '@library/shared-types';
|
|||||||
box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.3);
|
box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"]:invalid:not(:focus):not(:placeholder-shown),
|
||||||
|
.form-group textarea:invalid:not(:focus):not(:placeholder-shown) {
|
||||||
|
border-color: var(--error-colour, #c41e3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input[type="text"]:valid:not(:placeholder-shown),
|
||||||
|
.form-group textarea:valid:not(:placeholder-shown) {
|
||||||
|
border-color: rgba(46, 204, 113, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
@@ -274,7 +379,14 @@ export class SettingsComponent implements OnInit {
|
|||||||
displayName: '',
|
displayName: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
bio: '',
|
bio: '',
|
||||||
profilePublic: true
|
profilePublic: true,
|
||||||
|
website: '',
|
||||||
|
discordServer: '',
|
||||||
|
bluesky: '',
|
||||||
|
github: '',
|
||||||
|
linkedin: '',
|
||||||
|
twitch: '',
|
||||||
|
youtube: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -285,7 +397,14 @@ export class SettingsComponent implements OnInit {
|
|||||||
displayName: userData.displayName || '',
|
displayName: userData.displayName || '',
|
||||||
slug: userData.slug || '',
|
slug: userData.slug || '',
|
||||||
bio: userData.bio || '',
|
bio: userData.bio || '',
|
||||||
profilePublic: userData.profilePublic ?? true
|
profilePublic: userData.profilePublic ?? true,
|
||||||
|
website: userData.website || '',
|
||||||
|
discordServer: userData.discordServer || '',
|
||||||
|
bluesky: userData.bluesky || '',
|
||||||
|
github: userData.github || '',
|
||||||
|
linkedin: userData.linkedin || '',
|
||||||
|
twitch: userData.twitch || '',
|
||||||
|
youtube: userData.youtube || ''
|
||||||
};
|
};
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
},
|
},
|
||||||
@@ -304,7 +423,14 @@ export class SettingsComponent implements OnInit {
|
|||||||
displayName: this.formData.displayName || undefined,
|
displayName: this.formData.displayName || undefined,
|
||||||
slug: this.formData.slug || undefined,
|
slug: this.formData.slug || undefined,
|
||||||
bio: this.formData.bio || undefined,
|
bio: this.formData.bio || undefined,
|
||||||
profilePublic: this.formData.profilePublic
|
profilePublic: this.formData.profilePublic,
|
||||||
|
website: this.formData.website || undefined,
|
||||||
|
discordServer: this.formData.discordServer || undefined,
|
||||||
|
bluesky: this.formData.bluesky || undefined,
|
||||||
|
github: this.formData.github || undefined,
|
||||||
|
linkedin: this.formData.linkedin || undefined,
|
||||||
|
twitch: this.formData.twitch || undefined,
|
||||||
|
youtube: this.formData.youtube || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.userService.updateSettings(updates).subscribe({
|
this.userService.updateSettings(updates).subscribe({
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ export interface UserProfileResponse {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
badges: {
|
badges: {
|
||||||
isStaff: boolean;
|
isStaff: boolean;
|
||||||
isMod: boolean;
|
isMod: boolean;
|
||||||
@@ -36,6 +43,13 @@ export interface UpdateUserSettingsRequest {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
|||||||
@@ -35,6 +35,10 @@
|
|||||||
"@fastify/rate-limit": "10.3.0",
|
"@fastify/rate-limit": "10.3.0",
|
||||||
"@fastify/sensible": "6.0.4",
|
"@fastify/sensible": "6.0.4",
|
||||||
"@fastify/static": "9.0.0",
|
"@fastify/static": "9.0.0",
|
||||||
|
"@fortawesome/angular-fontawesome": "4.0.0",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "7.2.0",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "7.2.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
||||||
"@nhcarrigan/logger": "1.1.1",
|
"@nhcarrigan/logger": "1.1.1",
|
||||||
"@prisma/client": "6.19.2",
|
"@prisma/client": "6.19.2",
|
||||||
"dompurify": "3.3.1",
|
"dompurify": "3.3.1",
|
||||||
|
|||||||
Generated
+53
@@ -56,6 +56,18 @@ importers:
|
|||||||
'@fastify/static':
|
'@fastify/static':
|
||||||
specifier: 9.0.0
|
specifier: 9.0.0
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
|
'@fortawesome/angular-fontawesome':
|
||||||
|
specifier: 4.0.0
|
||||||
|
version: 4.0.0(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))
|
||||||
|
'@fortawesome/fontawesome-svg-core':
|
||||||
|
specifier: 7.2.0
|
||||||
|
version: 7.2.0
|
||||||
|
'@fortawesome/free-brands-svg-icons':
|
||||||
|
specifier: 7.2.0
|
||||||
|
version: 7.2.0
|
||||||
|
'@fortawesome/free-solid-svg-icons':
|
||||||
|
specifier: 7.2.0
|
||||||
|
version: 7.2.0
|
||||||
'@nhcarrigan/logger':
|
'@nhcarrigan/logger':
|
||||||
specifier: 1.1.1
|
specifier: 1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -1659,6 +1671,27 @@ packages:
|
|||||||
'@fastify/static@9.0.0':
|
'@fastify/static@9.0.0':
|
||||||
resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==}
|
resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==}
|
||||||
|
|
||||||
|
'@fortawesome/angular-fontawesome@4.0.0':
|
||||||
|
resolution: {integrity: sha512-TCqHqT5ovFY1A4RgMpoBUgS+RX3OVs39+CzHFgzDhbCPAopOa26J748TZJcuZwJAvGAk9tbWeVEmWuLByINAeg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@angular/core': ^21.0.0
|
||||||
|
|
||||||
|
'@fortawesome/fontawesome-common-types@7.2.0':
|
||||||
|
resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
'@fortawesome/fontawesome-svg-core@7.2.0':
|
||||||
|
resolution: {integrity: sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
'@fortawesome/free-brands-svg-icons@7.2.0':
|
||||||
|
resolution: {integrity: sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
'@fortawesome/free-solid-svg-icons@7.2.0':
|
||||||
|
resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
'@hapi/boom@10.0.1':
|
'@hapi/boom@10.0.1':
|
||||||
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
|
resolution: {integrity: sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==}
|
||||||
|
|
||||||
@@ -11651,6 +11684,26 @@ snapshots:
|
|||||||
fastq: 1.20.1
|
fastq: 1.20.1
|
||||||
glob: 13.0.1
|
glob: 13.0.1
|
||||||
|
|
||||||
|
'@fortawesome/angular-fontawesome@4.0.0(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))':
|
||||||
|
dependencies:
|
||||||
|
'@angular/core': 21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)
|
||||||
|
'@fortawesome/fontawesome-svg-core': 7.2.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@fortawesome/fontawesome-common-types@7.2.0': {}
|
||||||
|
|
||||||
|
'@fortawesome/fontawesome-svg-core@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@fortawesome/fontawesome-common-types': 7.2.0
|
||||||
|
|
||||||
|
'@fortawesome/free-brands-svg-icons@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@fortawesome/fontawesome-common-types': 7.2.0
|
||||||
|
|
||||||
|
'@fortawesome/free-solid-svg-icons@7.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@fortawesome/fontawesome-common-types': 7.2.0
|
||||||
|
|
||||||
'@hapi/boom@10.0.1':
|
'@hapi/boom@10.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@hapi/hoek': 11.0.7
|
'@hapi/hoek': 11.0.7
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ interface User {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic: boolean;
|
profilePublic: boolean;
|
||||||
|
website?: string;
|
||||||
|
discordServer?: string;
|
||||||
|
bluesky?: string;
|
||||||
|
github?: string;
|
||||||
|
linkedin?: string;
|
||||||
|
twitch?: string;
|
||||||
|
youtube?: string;
|
||||||
discordId: string;
|
discordId: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user