/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, inject, signal, OnInit } from '@angular/core'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faGlobe, faCloud, faFlag } 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 { AchievementService } from '../../services/achievement.service'; import { ToastService } from '../../services/toast.service'; import { AuthService } from '../../services/auth.service'; import { ReportModalComponent } from '../report-modal/report-modal.component'; import { PrimaryBadge, AchievementProgress } from '@library/shared-types'; @Component({ selector: 'app-profile', standalone: true, imports: [CommonModule, FontAwesomeModule, ReportModalComponent, RouterModule], template: `
@if (loading()) {
Loading profile...
} @else if (error()) {
{{ error() }}
} @else if (profile()) {
@if (profile()?.avatar) { } @else {
{{ profile()!.username[0]?.toUpperCase() }}
}

{{ profile()!.displayName || profile()!.username }}

@if (profile()!.displayName) {

\@{{ profile()!.username }}

} @if (profile()!.slug) {

library.nhcarrigan.com/profile/{{ profile()!.slug }}

}
@if (showReportButton()) { }
@if (profile()!.primaryBadge) {
@if (profile()!.primaryBadge === PrimaryBadge.STAFF && profile()!.badges.isStaff) { Staff } @if (profile()!.primaryBadge === PrimaryBadge.MOD && profile()!.badges.isMod) { Moderator } @if (profile()!.primaryBadge === PrimaryBadge.VIP && profile()!.badges.isVip) { VIP } @if (profile()!.primaryBadge === PrimaryBadge.DISCORD && profile()!.badges.inDiscord) { Discord Member }
} @if (profile()!.bio) {

{{ profile()!.bio }}

} @if (profile()!.website || profile()!.github || profile()!.bluesky || profile()!.linkedin || profile()!.twitch || profile()!.youtube || profile()!.discordServer) { }

Activity Statistics

{{ profile()!.stats.suggestionsCount }} Suggestions
{{ profile()!.stats.suggestionsAcceptedCount }} Accepted
{{ profile()!.stats.likesCount }} Likes
{{ profile()!.stats.commentsCount }} Comments
@if (profile()!.achievementPoints > 0) {
{{ profile()!.achievementPoints }} 🏆 Achievement Points
}
@if (recentAchievements().length > 0) {

Recent Achievements

@if (isOwnProfile()) { View All → }
@for (achievement of recentAchievements(); track achievement.definition.key) {
{{ achievement.definition.icon }}
{{ achievement.definition.title }}
{{ achievement.definition.points }} pts
}
}
} @if (reportModalOpen()) { }
`, styles: [` .profile-container { max-width: 800px; margin: 2rem auto; padding: 0 1rem; } .loading, .error { text-align: center; padding: 2rem; font-size: 1.2rem; } .error { color: var(--error-colour, #c41e3a); } .profile-card { background: var(--card-background, #1a1a2e); border-radius: 12px; padding: 2rem; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .profile-header { display: flex; align-items: center; gap: 1.5rem; margin-bottom: 1.5rem; position: relative; } .profile-avatar { width: 100px; height: 100px; border-radius: 50%; border: 3px solid var(--accent-colour, #9b59b6); } .profile-avatar-placeholder { width: 100px; height: 100px; border-radius: 50%; background: var(--accent-colour, #9b59b6); display: flex; align-items: center; justify-content: center; font-size: 2.5rem; font-weight: bold; color: white; } .profile-info { flex: 1; } .profile-username { margin: 0; font-size: 2rem; color: var(--text-colour, #e0e0e0); } .profile-handle { margin: 0.25rem 0; color: var(--text-muted, #a0a0a0); font-size: 1.1rem; } .profile-slug { margin: 0.25rem 0; color: var(--text-muted, #a0a0a0); font-size: 0.9rem; } .report-button { position: absolute; top: 0; right: 0; display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; background: linear-gradient(135deg, #c41e3a 0%, #e74c3c 100%); color: white; border: none; border-radius: 8px; font-size: 0.9rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .report-button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(196, 30, 58, 0.4); } .report-button fa-icon { font-size: 1rem; } .badges-section { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; } .badge { padding: 0.4rem 0.8rem; border-radius: 20px; font-size: 0.85rem; font-weight: 600; } .badge-staff { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .badge-mod { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; } .badge-vip { background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%); color: white; } .badge-member { background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333; } .bio-section { margin-bottom: 1.5rem; padding: 1rem; background: rgba(155, 89, 182, 0.1); border-radius: 8px; } .bio-text { margin: 0; color: var(--text-colour, #e0e0e0); line-height: 1.6; 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 { margin-bottom: 1.5rem; } .stats-section h2, .achievements-section h2 { margin: 0 0 1rem 0; color: var(--accent-colour, #9b59b6); font-size: 1.5rem; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 1rem; } .stat-card { text-align: center; padding: 1rem; background: rgba(155, 89, 182, 0.2); border-radius: 8px; border: 1px solid rgba(155, 89, 182, 0.3); } .stat-value { display: block; font-size: 2rem; font-weight: bold; color: var(--accent-colour, #9b59b6); } .stat-label { display: block; font-size: 0.9rem; color: var(--text-muted, #a0a0a0); margin-top: 0.25rem; } .footer-section { text-align: center; padding-top: 1rem; border-top: 1px solid rgba(155, 89, 182, 0.3); } .member-since { margin: 0; color: var(--text-muted, #a0a0a0); font-size: 0.9rem; } .achievement-points-card { background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%); border: 1px solid rgba(102, 126, 234, 0.5); } .achievements-section { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(155, 89, 182, 0.3); } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } .view-all-link { color: var(--accent-colour, #9b59b6); text-decoration: none; font-size: 0.9rem; transition: opacity 0.2s; } .view-all-link:hover { opacity: 0.8; } .achievements-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; } .achievement-badge { background: var(--card-background, #16213e); border-radius: 8px; padding: 1rem; display: flex; align-items: center; gap: 0.75rem; border: 2px solid transparent; transition: all 0.2s; } .achievement-badge[data-tier="bronze"] { border-color: #cd7f32; } .achievement-badge[data-tier="silver"] { border-color: #c0c0c0; } .achievement-badge[data-tier="gold"] { border-color: #ffd700; } .achievement-badge[data-tier="platinum"] { border-color: #e5e4e2; } .achievement-badge[data-tier="diamond"] { border-color: #b9f2ff; } .achievement-badge:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(155, 89, 182, 0.3); } .achievement-icon { font-size: 2rem; flex-shrink: 0; } .achievement-info { flex: 1; min-width: 0; } .achievement-title { font-weight: 600; color: var(--text-colour, #ffffff); font-size: 1rem; line-height: 1.3; word-wrap: break-word; } .achievement-points { color: var(--text-colour, #ffffff); opacity: 0.8; font-size: 0.85rem; margin-top: 0.25rem; font-weight: 500; } `] }) export class ProfileComponent implements OnInit { private route = inject(ActivatedRoute); private userService = inject(UserService); private achievementService = inject(AchievementService); private toastService = inject(ToastService); private authService = inject(AuthService); profile = signal(null); recentAchievements = signal([]); loading = signal(true); error = signal(null); reportModalOpen = signal(false); // Expose PrimaryBadge enum for template readonly PrimaryBadge = PrimaryBadge; // Font Awesome icons faGlobe = faGlobe; faGithub = faGithub; faCloud = faCloud; faLinkedin = faLinkedin; faTwitch = faTwitch; faYoutube = faYoutube; faDiscord = faDiscord; faFlag = faFlag; ngOnInit(): void { const identifier = this.route.snapshot.paramMap.get('identifier'); if (!identifier) { this.error.set('No user identifier provided'); this.loading.set(false); return; } this.userService.getProfile(identifier).subscribe({ next: (profileData: UserProfileResponse) => { this.profile.set(profileData); this.loading.set(false); // Load recent achievements this.achievementService.getUserProgress(profileData.id).subscribe({ next: (achievements) => { // Get earned achievements sorted by earned date, take top 6 const earned = achievements .filter(a => a.earned && a.earnedAt) .sort((a, b) => { const dateA = a.earnedAt ? new Date(a.earnedAt).getTime() : 0; const dateB = b.earnedAt ? new Date(b.earnedAt).getTime() : 0; return dateB - dateA; }) .slice(0, 6); this.recentAchievements.set(earned); }, error: (err) => { console.error('Error loading achievements:', err); // Don't show error toast for achievements failure } }); }, error: (err: Error) => { console.error('Error loading profile:', err); this.error.set('Failed to load profile'); this.loading.set(false); this.toastService.error('Failed to load profile'); } }); } formatDate(date: Date | string): string { return new Date(date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); } /** * Determine whether to show the report button. * Only show if the user is authenticated and viewing someone else's profile. */ showReportButton(): boolean { const currentUser = this.authService.user(); const profileData = this.profile(); if (!currentUser || !profileData) { return false; } // Don't show report button on your own profile return currentUser.id !== profileData.id; } /** * Determine whether viewing your own profile. */ isOwnProfile(): boolean { const currentUser = this.authService.user(); const profileData = this.profile(); if (!currentUser || !profileData) { return false; } return currentUser.id === profileData.id; } /** * Open the report modal. */ openReportModal(): void { this.reportModalOpen.set(true); } /** * Close the report modal. */ closeReportModal(): void { this.reportModalOpen.set(false); } }