/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan */ import { Component, OnInit, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AchievementCategory, AchievementProgress, AchievementTier, } from '@library/shared-types'; import { AchievementService } from '../../services/achievement.service'; @Component({ selector: 'app-achievements', standalone: true, imports: [CommonModule], template: `

🏆 Achievements

Track your progress across all achievement categories

@if (totalPoints() > 0) {
Total Points: {{ totalPoints() }}
}
@if (loading()) {
Loading achievements...
} @else if (error()) {
{{ error() }}
} @else {
@for (achievement of filteredAchievements(); track achievement.definition.key) {
{{ achievement.definition.icon }}

{{ achievement.definition.title }} @if (achievement.earned) { }

{{ achievement.definition.description }}

@if (!achievement.earned && achievement.progress > 0) {
{{ achievement.progress }}% complete
} @if (achievement.earned && achievement.earnedAt) {
Earned {{ formatDate(achievement.earnedAt) }}
}
}
}
`, styles: [` .achievements-container { max-width: 1400px; margin: 0 auto; padding: 2rem; } .achievements-header { text-align: center; margin-bottom: 2rem; } .achievements-header h1 { font-size: 2.5rem; margin-bottom: 0.5rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .subtitle { color: #a0aec0; font-size: 1.1rem; } .total-points { margin-top: 1rem; padding: 1rem 2rem; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: inline-block; color: white; font-size: 1.2rem; font-weight: bold; } .points-label { margin-right: 0.5rem; } .points-value { font-size: 1.5rem; } .filter-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; margin-bottom: 2rem; } .filter-buttons button { padding: 0.75rem 1.5rem; background: #2d3748; color: #cbd5e0; border: 2px solid #4a5568; border-radius: 8px; cursor: pointer; transition: all 0.2s; font-size: 1rem; } .filter-buttons button:hover { background: #4a5568; border-color: #667eea; } .filter-buttons button.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-color: #667eea; color: white; } .achievements-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; } .achievement-card { background: #2d3748; border-radius: 12px; padding: 1.5rem; display: flex; gap: 1rem; border: 2px solid #4a5568; transition: all 0.3s; } .achievement-card.earned { background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); border-color: #667eea; } .achievement-card.earned:hover { transform: translateY(-4px); box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); } .achievement-card.locked { opacity: 0.6; } .achievement-icon { font-size: 3rem; flex-shrink: 0; } .achievement-content { flex: 1; } .achievement-title { font-size: 1.25rem; margin-bottom: 0.5rem; color: #e2e8f0; display: flex; align-items: center; gap: 0.5rem; } .earned-check { color: #48bb78; font-size: 1.5rem; } .achievement-description { color: #a0aec0; margin-bottom: 1rem; line-height: 1.5; } .achievement-footer { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } .achievement-tier { padding: 0.25rem 0.75rem; border-radius: 6px; font-size: 0.875rem; font-weight: 600; text-transform: uppercase; } .achievement-tier[data-tier="bronze"] { background: linear-gradient(135deg, #cd7f32 0%, #b87333 100%); color: white; } .achievement-tier[data-tier="silver"] { background: linear-gradient(135deg, #c0c0c0 0%, #a8a8a8 100%); color: #2d3748; } .achievement-tier[data-tier="gold"] { background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); color: #2d3748; } .achievement-tier[data-tier="platinum"] { background: linear-gradient(135deg, #e5e4e2 0%, #bdb8af 100%); color: #2d3748; } .achievement-tier[data-tier="diamond"] { background: linear-gradient(135deg, #b9f2ff 0%, #7ec8e3 100%); color: #2d3748; } .achievement-points { color: #667eea; font-weight: 600; } .progress-bar { width: 100%; height: 8px; background: #4a5568; border-radius: 4px; overflow: hidden; margin: 0.5rem 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); transition: width 0.3s; } .progress-text { font-size: 0.875rem; color: #a0aec0; text-align: right; } .earned-date { font-size: 0.875rem; color: #48bb78; margin-top: 0.5rem; } .loading, .error { text-align: center; padding: 3rem; font-size: 1.2rem; color: #a0aec0; } .error { color: #fc8181; } `], }) export class AchievementsComponent implements OnInit { private readonly achievementService = inject(AchievementService); achievements = signal([]); selectedCategory = signal(null); loading = signal(true); error = signal(null); // Expose AchievementCategory enum for template readonly AchievementCategory = AchievementCategory; ngOnInit(): void { this.loadAchievements(); } private loadAchievements(): void { this.achievementService.getCurrentUserProgress().subscribe({ next: (achievements) => { this.achievements.set(achievements); this.loading.set(false); }, error: (err) => { this.error.set('Failed to load achievements'); this.loading.set(false); console.error('Error loading achievements:', err); }, }); } filteredAchievements(): AchievementProgress[] { const category = this.selectedCategory(); if (!category) { return this.achievements(); } return this.achievements().filter( (a) => a.definition.category === category, ); } selectCategory(category: AchievementCategory | null): void { this.selectedCategory.set(category); } totalAchievements(): number { return this.achievements().length; } totalEarned(): number { return this.achievements().filter((a) => a.earned).length; } totalPoints(): number { return this.achievements() .filter((a) => a.earned) .reduce((sum, a) => sum + a.definition.points, 0); } getCategoryCount(category: AchievementCategory): string { const total = this.achievements().filter( (a) => a.definition.category === category, ).length; const earned = this.achievements().filter( (a) => a.definition.category === category && a.earned, ).length; return `${earned}/${total}`; } getTierLabel(tier: AchievementTier): string { return tier; } formatDate(date: Date): string { const d = new Date(date); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) { return 'today'; } if (diffDays === 1) { return 'yesterday'; } if (diffDays < 7) { return `${diffDays} days ago`; } if (diffDays < 30) { const weeks = Math.floor(diffDays / 7); return `${weeks} ${weeks === 1 ? 'week' : 'weeks'} ago`; } if (diffDays < 365) { const months = Math.floor(diffDays / 30); return `${months} ${months === 1 ? 'month' : 'months'} ago`; } const years = Math.floor(diffDays / 365); return `${years} ${years === 1 ? 'year' : 'years'} ago`; } }