/** * @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 { RouterLink } from '@angular/router'; import type { Activity } from '@library/shared-types'; import { ActivityType } from '@library/shared-types'; import { ActivityService } from '../../services/activity.service'; import { SanitizeService } from '../../services/sanitize.service'; @Component({ selector: 'app-activity-feed', standalone: true, imports: [CommonModule, RouterLink], template: `

Recent Activity

See what's happening in the library community

@if (loading()) {

Loading activities...

} @else if (activities().length === 0) {

No recent activity to display.

} @else {
@for (activity of activities(); track activity.id) {
{{ formatTime(activity.createdAt) }}
@switch (activity.type) { @case (ActivityType.suggestion) {
💡 suggested {{ activity.suggestionTitle }} {{ formatStatus(activity.status) }}
} @case (ActivityType.like) { } @case (ActivityType.comment) { } @case (ActivityType.achievement) {
{{ activity.achievementIcon }} earned the {{ activity.achievementName }} achievement ({{ activity.achievementPoints }} pts)
} }
}
@if (hasMore()) {
} }
`, styles: [` .activity-container { max-width: 800px; margin: 2rem auto; padding: 0 1rem; } h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #1f2937; } .subtitle { color: #6b7280; margin-bottom: 2rem; } .loading, .no-activities { text-align: center; padding: 3rem; color: #6b7280; font-size: 1.1rem; } .activity-feed { display: flex; flex-direction: column; gap: 1rem; } .activity-card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); border: 1px solid #e5e7eb; } .activity-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; } .user-info { display: flex; align-items: center; gap: 0.75rem; } .user-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .user-avatar-placeholder { width: 40px; height: 40px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 1.2rem; } .user-details { display: flex; flex-direction: column; gap: 0.25rem; } .username { font-weight: 600; color: #1f2937; text-decoration: none; } .username:hover { color: #10b981; } .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; } .badge-staff { background: #ef4444; color: white; } .badge-mod { background: #3b82f6; color: white; } .badge-vip { background: #f59e0b; color: white; } .badge-discord { background: #5865f2; color: white; } .timestamp { font-size: 0.875rem; color: #9ca3af; } .activity-content { padding-left: 55px; } .activity-suggestion, .activity-like, .activity-comment-header, .activity-achievement { display: flex; align-items: start; gap: 0.75rem; } .activity-comment { display: flex; flex-direction: column; gap: 0; } .activity-icon { font-size: 1.5rem; line-height: 1; } .activity-text { color: #4b5563; line-height: 1.6; } .activity-text strong { color: #1f2937; font-weight: 600; } .entity-link { color: #10b981; text-decoration: none; font-weight: 500; } .entity-link:hover { text-decoration: underline; } .comment-preview { margin-top: 0.5rem; margin-left: 55px; padding: 0.75rem; background: #f9fafb; border-left: 3px solid #10b981; border-radius: 4px; color: #4b5563; } .status-badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; margin-left: 0.5rem; } .status-unreviewed { background: #fef3c7; color: #92400e; } .status-accepted { background: #d1fae5; color: #065f46; } .status-declined { background: #fee2e2; color: #991b1b; } .points { color: #10b981; font-weight: 600; margin-left: 0.25rem; } .load-more-container { display: flex; justify-content: center; margin-top: 2rem; } .btn { padding: 0.75rem 1.5rem; border: none; border-radius: 6px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s; } .btn-primary { background: #10b981; color: white; } .btn-primary:hover:not(:disabled) { background: #059669; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } `] }) export class ActivityFeedComponent implements OnInit { private activityService = inject(ActivityService); public sanitizeService = inject(SanitizeService); // Make ActivityType accessible in template ActivityType = ActivityType; activities = signal([]); loading = signal(true); loadingMore = signal(false); hasMore = signal(false); offset = 0; limit = 50; ngOnInit() { this.loadActivities(); } loadActivities() { this.activityService.getActivityFeed(this.limit, this.offset).subscribe({ next: (response) => { this.activities.set(response.activities); this.hasMore.set(response.hasMore); this.loading.set(false); }, error: () => { this.loading.set(false); } }); } loadMore() { this.loadingMore.set(true); this.offset += this.limit; this.activityService.getActivityFeed(this.limit, this.offset).subscribe({ next: (response) => { this.activities.update(current => [...current, ...response.activities]); this.hasMore.set(response.hasMore); this.loadingMore.set(false); }, error: () => { this.loadingMore.set(false); } }); } formatTime(date: Date): string { const now = new Date(); const activityDate = new Date(date); const diffMs = now.getTime() - activityDate.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return activityDate.toLocaleDateString(); } formatStatus(status: string): string { switch (status) { case 'UNREVIEWED': return 'Pending'; case 'ACCEPTED': return 'Accepted'; case 'DECLINED': return 'Declined'; default: return status; } } }