/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, inject, signal, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { UserService, UpdateUserSettingsRequest } from '../../services/user.service'; import { AuthService } from '../../services/auth.service'; import { ToastService } from '../../services/toast.service'; import { User, PrimaryBadge } from '@library/shared-types'; @Component({ selector: 'app-settings', standalone: true, imports: [CommonModule, FormsModule], template: `

Profile Settings

@if (loading()) {
Loading settings...
} @else if (user()) {

Profile Information

This will be shown instead of your username
Your profile will be at: library.nhcarrigan.com/profile/{{ formData.slug || 'your-slug' }} Only lowercase letters, numbers, and hyphens allowed
{{ (formData.bio?.length || 0) }} / 500 characters
Choose one badge to display on your profile and comments

Social Links

Your personal website or portfolio (must start with http:// or https://)
Just your GitHub username (alphanumeric and hyphens, 1-39 characters)
Your full Bluesky handle (e.g., username.bsky.social)
Just your LinkedIn username (alphanumeric and hyphens, 3-100 characters)
Just your Twitch username (alphanumeric and underscores, 4-25 characters)
Your YouTube handle (@username) or channel ID (UC...)
Just your Discord server invite code (alphanumeric, 2-32 characters)

Privacy

When disabled, only you can view your profile

Account Information

Username: {{ user()!.username }}
Email: {{ user()!.email }}
@if (user()!.avatar) {
Avatar: Avatar
} Username, email, and avatar are managed through Discord and cannot be changed here
}
`, styles: [` .settings-container { max-width: 700px; margin: 2rem auto; padding: 0 1rem; } h1 { color: var(--accent-colour, #9b59b6); margin-bottom: 2rem; } .loading { text-align: center; padding: 2rem; font-size: 1.2rem; } .settings-form { background: var(--card-background, #1a1a2e); border-radius: 12px; padding: 2rem; } .form-section { margin-bottom: 2rem; padding-bottom: 2rem; border-bottom: 1px solid rgba(155, 89, 182, 0.3); } .form-section:last-of-type { border-bottom: none; } .form-section h2 { color: var(--accent-colour, #9b59b6); font-size: 1.3rem; margin-bottom: 1rem; } .form-group { margin-bottom: 1.5rem; } .form-group label { display: block; margin-bottom: 0.5rem; color: var(--text-colour, #e0e0e0); font-weight: 600; } .form-group input[type="text"], .form-group textarea, .form-group select { width: 100%; padding: 0.75rem; border: 1px solid rgba(155, 89, 182, 0.5); border-radius: 8px; background: rgba(0, 0, 0, 0.2); color: var(--text-colour, #e0e0e0); font-size: 1rem; font-family: inherit; } .form-group select { cursor: pointer; } .form-group select option { background: #1a1a2e; color: var(--text-colour, #e0e0e0); } .form-group input[type="text"]:focus, .form-group textarea:focus, .form-group select:focus { outline: none; border-color: var(--accent-colour, #9b59b6); 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 { resize: vertical; min-height: 100px; } .form-help { display: block; margin-top: 0.25rem; color: var(--text-muted, #a0a0a0); font-size: 0.85rem; } .checkbox-group label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; } .checkbox-group input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; } .info-item { margin-bottom: 1rem; color: var(--text-colour, #e0e0e0); } .info-item strong { color: var(--accent-colour, #9b59b6); } .avatar-preview { width: 50px; height: 50px; border-radius: 50%; margin-left: 0.5rem; vertical-align: middle; } .form-actions { display: flex; justify-content: flex-end; padding-top: 1rem; } .btn { padding: 0.75rem 2rem; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; } .btn-primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .btn-primary:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .btn:disabled { opacity: 0.5; cursor: not-allowed; } `] }) export class SettingsComponent implements OnInit { private userService = inject(UserService); private authService = inject(AuthService); private toastService = inject(ToastService); user = signal(null); loading = signal(true); saving = signal(false); // Expose PrimaryBadge enum for template readonly PrimaryBadge = PrimaryBadge; formData: UpdateUserSettingsRequest & { bio?: string } = { displayName: '', slug: '', bio: '', profilePublic: true, primaryBadge: undefined, website: '', discordServer: '', bluesky: '', github: '', linkedin: '', twitch: '', youtube: '' }; ngOnInit(): void { this.userService.getMe().subscribe({ next: (userData: User) => { this.user.set(userData); this.formData = { displayName: userData.displayName || '', slug: userData.slug || '', bio: userData.bio || '', profilePublic: userData.profilePublic ?? true, primaryBadge: userData.primaryBadge || undefined, 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); }, error: (err: Error) => { console.error('Error loading user profile:', err); this.toastService.error('Failed to load profile'); this.loading.set(false); } }); } saveSettings(): void { this.saving.set(true); const updates: UpdateUserSettingsRequest = { displayName: this.formData.displayName || undefined, slug: this.formData.slug || undefined, bio: this.formData.bio || undefined, profilePublic: this.formData.profilePublic, primaryBadge: this.formData.primaryBadge || undefined, 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({ next: (updatedUser: User) => { this.user.set(updatedUser); this.authService.updateUser(updatedUser); this.saving.set(false); this.toastService.success('Settings saved successfully!'); }, error: (err: Error) => { console.error('Error saving settings:', err); this.toastService.error('Failed to save settings'); this.saving.set(false); } }); } }