feat: implement user profiles with achievements and primary badge system #58

Merged
naomi merged 17 commits from feat/user-profiles into main 2026-02-19 22:21:18 -08:00
Showing only changes of commit e728968fc9 - Show all commits
@@ -9,17 +9,39 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { ReportService } from '../../services/report.service';
import { CommentReportService } from '../../services/comment-report.service';
import { AuthService } from '../../services/auth.service';
import { ToastService } from '../../services/toast.service';
import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/shared-types';
import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportReason } from '@library/shared-types';
@Component({
selector: 'app-admin-reports',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="admin-reports-container">
<h1>Profile Reports</h1>
<div class="admin-reports-wrapper">
<div class="admin-reports-container">
<div class="header-section">
<h1>Reports</h1>
<div class="report-type-toggle">
<button
type="button"
class="toggle-btn"
[class.active]="reportType() === 'profile'"
(click)="setReportType('profile')"
>
Profile Reports
</button>
<button
type="button"
class="toggle-btn"
[class.active]="reportType() === 'comment'"
(click)="setReportType('comment')"
>
Comment Reports
</button>
</div>
</div>
@if (loading()) {
<div class="loading" role="status" aria-live="polite">
@@ -40,7 +62,7 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
[attr.aria-selected]="activeFilter() === 'all'"
(click)="setFilter('all')"
>
All ({{ allReports().length }})
All ({{ reportType() === 'profile' ? allProfileReports().length : allCommentReports().length }})
</button>
<button
type="button"
@@ -86,126 +108,239 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
<!-- Reports List -->
<div class="reports-list" role="region" aria-label="Reports list">
@for (report of filteredReports(); track report.id) {
<div class="report-card">
<!-- Report Header -->
<div class="report-header">
<div class="user-info">
<div class="user-section">
<h3>Reported User</h3>
<div class="user-details">
@if (report.reportedUser.avatar) {
<img
[src]="report.reportedUser.avatar"
[alt]="report.reportedUser.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reportedUser.username + ' avatar'">
{{ report.reportedUser.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reportedUser.username }}</span>
@if (report.reportedUser.displayName) {
<span class="display-name">({{ report.reportedUser.displayName }})</span>
@if (reportType() === 'profile') {
@for (report of filteredProfileReports(); track report.id) {
<div class="report-card">
<!-- Report Header -->
<div class="report-header">
<div class="user-info">
<div class="user-section">
<h3>Reported User</h3>
<div class="user-details">
@if (report.reportedUser.avatar) {
<img
[src]="report.reportedUser.avatar"
[alt]="report.reportedUser.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reportedUser.username + ' avatar'">
{{ report.reportedUser.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reportedUser.username }}</span>
@if (report.reportedUser.displayName) {
<span class="display-name">({{ report.reportedUser.displayName }})</span>
}
</div>
</div>
</div>
<div class="user-section">
<h3>Reporter</h3>
<div class="user-details">
@if (report.reporter.avatar) {
<img
[src]="report.reporter.avatar"
[alt]="report.reporter.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reporter.username + ' avatar'">
{{ report.reporter.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reporter.username }}</span>
@if (report.reporter.displayName) {
<span class="display-name">({{ report.reporter.displayName }})</span>
}
</div>
</div>
</div>
</div>
<div class="user-section">
<h3>Reporter</h3>
<div class="user-details">
@if (report.reporter.avatar) {
<img
[src]="report.reporter.avatar"
[alt]="report.reporter.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reporter.username + ' avatar'">
{{ report.reporter.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reporter.username }}</span>
@if (report.reporter.displayName) {
<span class="display-name">({{ report.reporter.displayName }})</span>
}
</div>
</div>
<div class="report-meta">
<span
class="status-badge"
[class.pending]="report.status === 'PENDING'"
[class.reviewed]="report.status === 'REVIEWED'"
[class.dismissed]="report.status === 'DISMISSED'"
[class.action-taken]="report.status === 'ACTION_TAKEN'"
[attr.aria-label]="'Status: ' + formatStatus(report.status)"
>
{{ formatStatus(report.status) }}
</span>
<span class="report-date">
{{ formatDate(report.createdAt) }}
</span>
</div>
</div>
<div class="report-meta">
<span
class="status-badge"
[class.pending]="report.status === 'PENDING'"
[class.reviewed]="report.status === 'REVIEWED'"
[class.dismissed]="report.status === 'DISMISSED'"
[class.action-taken]="report.status === 'ACTION_TAKEN'"
[attr.aria-label]="'Status: ' + formatStatus(report.status)"
<!-- Report Content -->
<div class="report-content">
<div class="reason">
<strong>Reason:</strong> {{ formatReason(report.reason) }}
</div>
<div class="details">
<strong>Details:</strong>
<p>{{ report.details }}</p>
</div>
@if (report.reviewNotes) {
<div class="review-notes">
<strong>Review Notes:</strong>
<p>{{ report.reviewNotes }}</p>
@if (report.reviewer) {
<small class="reviewer">
Reviewed by {{ report.reviewer.username }}
</small>
}
</div>
}
</div>
<!-- Report Actions -->
<div class="report-actions">
<button
type="button"
class="btn btn-review"
(click)="openProfileReviewModal(report)"
[attr.aria-label]="'Review report for ' + report.reportedUser.username"
>
{{ formatStatus(report.status) }}
</span>
<span class="report-date">
{{ formatDate(report.createdAt) }}
</span>
Review
</button>
</div>
</div>
} @empty {
<div class="empty-state">
<p>No profile reports found.</p>
</div>
}
} @else {
@for (report of filteredCommentReports(); track report.id) {
<div class="report-card">
<!-- Report Header -->
<div class="report-header">
<div class="user-info">
<div class="user-section">
<h3>Comment Author</h3>
<div class="user-details">
@if (report.reportedComment.user.avatar) {
<img
[src]="report.reportedComment.user.avatar"
[alt]="report.reportedComment.user.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reportedComment.user.username + ' avatar'">
{{ report.reportedComment.user.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reportedComment.user.username }}</span>
</div>
</div>
</div>
<!-- Report Content -->
<div class="report-content">
<div class="reason">
<strong>Reason:</strong> {{ formatReason(report.reason) }}
</div>
<div class="details">
<strong>Details:</strong>
<p>{{ report.details }}</p>
</div>
@if (report.reviewNotes) {
<div class="review-notes">
<strong>Review Notes:</strong>
<p>{{ report.reviewNotes }}</p>
@if (report.reviewer) {
<small class="reviewer">
Reviewed by {{ report.reviewer.username }}
</small>
}
<div class="user-section">
<h3>Reporter</h3>
<div class="user-details">
@if (report.reporter.avatar) {
<img
[src]="report.reporter.avatar"
[alt]="report.reporter.username + ' avatar'"
class="avatar"
/>
} @else {
<div class="avatar-placeholder" [attr.aria-label]="report.reporter.username + ' avatar'">
{{ report.reporter.username.charAt(0).toUpperCase() }}
</div>
}
<div class="user-names">
<span class="username">{{ report.reporter.username }}</span>
@if (report.reporter.displayName) {
<span class="display-name">({{ report.reporter.displayName }})</span>
}
</div>
</div>
</div>
</div>
}
</div>
<!-- Report Actions -->
<div class="report-actions">
<button
type="button"
class="btn btn-review"
(click)="openReviewModal(report)"
[attr.aria-label]="'Review report for ' + report.reportedUser.username"
>
Review
</button>
<div class="report-meta">
<span
class="status-badge"
[class.pending]="report.status === 'PENDING'"
[class.reviewed]="report.status === 'REVIEWED'"
[class.dismissed]="report.status === 'DISMISSED'"
[class.action-taken]="report.status === 'ACTION_TAKEN'"
[attr.aria-label]="'Status: ' + formatStatus(report.status)"
>
{{ formatStatus(report.status) }}
</span>
<span class="report-date">
{{ formatDate(report.createdAt) }}
</span>
</div>
</div>
<!-- Report Content -->
<div class="report-content">
<div class="comment-content-preview">
<strong>Comment:</strong>
<p class="comment-text">{{ truncateComment(report.reportedComment.content) }}</p>
</div>
<div class="reason">
<strong>Reason:</strong> {{ formatReason(report.reason) }}
</div>
<div class="details">
<strong>Details:</strong>
<p>{{ report.details }}</p>
</div>
@if (report.reviewNotes) {
<div class="review-notes">
<strong>Review Notes:</strong>
<p>{{ report.reviewNotes }}</p>
@if (report.reviewer) {
<small class="reviewer">
Reviewed by {{ report.reviewer.username }}
</small>
}
</div>
}
</div>
<!-- Report Actions -->
<div class="report-actions">
<button
type="button"
class="btn btn-review"
(click)="openCommentReviewModal(report)"
[attr.aria-label]="'Review report for comment by ' + report.reportedComment.user.username"
>
Review
</button>
</div>
</div>
</div>
} @empty {
<div class="empty-state">
<p>No reports found.</p>
</div>
} @empty {
<div class="empty-state">
<p>No comment reports found.</p>
</div>
}
}
</div>
}
</div>
<!-- Review Modal -->
@if (reviewingReport()) {
<!-- Profile Review Modal -->
@if (reviewingProfileReport()) {
<div
class="modal-overlay"
(click)="closeReviewModal()"
(keydown.escape)="closeReviewModal()"
(click)="closeProfileReviewModal()"
(keydown.escape)="closeProfileReviewModal()"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-labelledby="profile-modal-title"
>
<div
class="modal-content"
@@ -215,11 +350,11 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
tabindex="-1"
>
<div class="modal-header">
<h2 id="modal-title">Review Report</h2>
<h2 id="profile-modal-title">Review Profile Report</h2>
<button
type="button"
class="modal-close"
(click)="closeReviewModal()"
(click)="closeProfileReviewModal()"
aria-label="Close modal"
>
×
@@ -231,29 +366,29 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
<div class="review-summary">
<div class="summary-item">
<strong>Reported User:</strong>
<span>{{ reviewingReport()!.reportedUser.username }}</span>
<span>{{ reviewingProfileReport()!.reportedUser.username }}</span>
</div>
<div class="summary-item">
<strong>Reporter:</strong>
<span>{{ reviewingReport()!.reporter.username }}</span>
<span>{{ reviewingProfileReport()!.reporter.username }}</span>
</div>
<div class="summary-item">
<strong>Reason:</strong>
<span>{{ formatReason(reviewingReport()!.reason) }}</span>
<span>{{ formatReason(reviewingProfileReport()!.reason) }}</span>
</div>
<div class="summary-item">
<strong>Reported On:</strong>
<span>{{ formatDate(reviewingReport()!.createdAt) }}</span>
<span>{{ formatDate(reviewingProfileReport()!.createdAt) }}</span>
</div>
</div>
<div class="details-section">
<strong>Details:</strong>
<p class="details-text">{{ reviewingReport()!.details }}</p>
<p class="details-text">{{ reviewingProfileReport()!.details }}</p>
</div>
<!-- Review Form -->
<form (ngSubmit)="submitReview()" class="review-form">
<form (ngSubmit)="submitProfileReview()" class="review-form">
<div class="form-group">
<label for="status">Update Status</label>
<select
@@ -287,7 +422,121 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
<button
type="button"
class="btn btn-secondary"
(click)="closeReviewModal()"
(click)="closeProfileReviewModal()"
[disabled]="submitting()"
>
Cancel
</button>
<button
type="submit"
class="btn btn-primary"
[disabled]="submitting()"
>
{{ submitting() ? 'Submitting...' : 'Submit Review' }}
</button>
</div>
</form>
</div>
</div>
</div>
}
<!-- Comment Review Modal -->
@if (reviewingCommentReport()) {
<div
class="modal-overlay"
(click)="closeCommentReviewModal()"
(keydown.escape)="closeCommentReviewModal()"
role="dialog"
aria-modal="true"
aria-labelledby="comment-modal-title"
>
<div
class="modal-content"
(click)="$event.stopPropagation()"
(keydown)="$event.stopPropagation()"
role="document"
tabindex="-1"
>
<div class="modal-header">
<h2 id="comment-modal-title">Review Comment Report</h2>
<button
type="button"
class="modal-close"
(click)="closeCommentReviewModal()"
aria-label="Close modal"
>
×
</button>
</div>
<div class="modal-body">
<!-- Report Summary -->
<div class="review-summary">
<div class="summary-item">
<strong>Comment Author:</strong>
<span>{{ reviewingCommentReport()!.reportedComment.user.username }}</span>
</div>
<div class="summary-item">
<strong>Reporter:</strong>
<span>{{ reviewingCommentReport()!.reporter.username }}</span>
</div>
<div class="summary-item">
<strong>Reason:</strong>
<span>{{ formatReason(reviewingCommentReport()!.reason) }}</span>
</div>
<div class="summary-item">
<strong>Reported On:</strong>
<span>{{ formatDate(reviewingCommentReport()!.createdAt) }}</span>
</div>
</div>
<div class="comment-full-content">
<strong>Full Comment:</strong>
<p class="comment-text">{{ reviewingCommentReport()!.reportedComment.content }}</p>
</div>
<div class="details-section">
<strong>Report Details:</strong>
<p class="details-text">{{ reviewingCommentReport()!.details }}</p>
</div>
<!-- Review Form -->
<form (ngSubmit)="submitCommentReview()" class="review-form">
<div class="form-group">
<label for="comment-status">Update Status</label>
<select
id="comment-status"
name="status"
[(ngModel)]="reviewForm.status"
class="form-control"
required
aria-required="true"
>
<option value="PENDING">Pending</option>
<option value="REVIEWED">Reviewed</option>
<option value="DISMISSED">Dismissed</option>
<option value="ACTION_TAKEN">Action Taken</option>
</select>
</div>
<div class="form-group">
<label for="comment-reviewNotes">Review Notes (Optional)</label>
<textarea
id="comment-reviewNotes"
name="reviewNotes"
[(ngModel)]="reviewForm.reviewNotes"
class="form-control"
rows="4"
placeholder="Add any notes about your review..."
></textarea>
</div>
<div class="modal-actions">
<button
type="button"
class="btn btn-secondary"
(click)="closeCommentReviewModal()"
[disabled]="submitting()"
>
Cancel
@@ -308,18 +557,66 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
</div>
`,
styles: [`
.admin-reports-wrapper {
position: relative;
}
.admin-reports-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
h1 {
color: var(--witch-purple);
margin-bottom: 2rem;
margin: 0;
font-size: 2rem;
}
.report-type-toggle {
display: flex;
gap: 0.5rem;
background: var(--witch-lavender);
padding: 0.25rem;
border-radius: 12px;
}
.toggle-btn {
padding: 0.75rem 1.5rem;
background: transparent;
border: none;
border-radius: 8px;
color: var(--witch-mauve);
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.toggle-btn:hover {
color: var(--witch-purple);
}
.toggle-btn.active {
background: var(--witch-purple);
color: var(--witch-moon);
font-weight: 600;
}
.toggle-btn:focus {
outline: 2px solid var(--witch-purple);
outline-offset: 2px;
}
.loading, .error, .empty-state {
text-align: center;
padding: 3rem 2rem;
@@ -535,6 +832,41 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
font-style: italic;
}
.comment-content-preview {
padding: 1rem;
background: var(--witch-lavender);
border-radius: 8px;
margin-bottom: 1rem;
}
.comment-content-preview strong {
display: block;
margin-bottom: 0.5rem;
color: var(--witch-purple);
font-weight: 600;
}
.comment-text {
color: var(--witch-mauve);
line-height: 1.6;
margin: 0;
word-wrap: break-word;
}
.comment-full-content {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--witch-lavender);
border-radius: 8px;
}
.comment-full-content strong {
display: block;
margin-bottom: 0.5rem;
color: var(--witch-purple);
font-weight: 600;
}
/* Report Actions */
.report-actions {
display: flex;
@@ -578,17 +910,21 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
z-index: 9999 !important;
padding: 1rem;
margin: 0 !important;
transform: none !important;
}
.modal-content {
@@ -813,6 +1149,7 @@ import { ProfileReportWithUsers, ReportStatus, ReportReason } from '@library/sha
})
export class AdminReportsComponent implements OnInit {
private reportService = inject(ReportService);
private commentReportService = inject(CommentReportService);
private authService = inject(AuthService);
private toastService = inject(ToastService);
private router = inject(Router);
@@ -820,12 +1157,15 @@ export class AdminReportsComponent implements OnInit {
// Make ReportStatus accessible in template
protected readonly ReportStatus = ReportStatus;
allReports = signal<ProfileReportWithUsers[]>([]);
reportType = signal<'profile' | 'comment'>('profile');
allProfileReports = signal<ProfileReportWithUsers[]>([]);
allCommentReports = signal<CommentReportWithDetails[]>([]);
loading = signal(true);
error = signal<string | null>(null);
activeFilter = signal<'all' | ReportStatus>('all');
reviewingReport = signal<ProfileReportWithUsers | null>(null);
reviewingProfileReport = signal<ProfileReportWithUsers | null>(null);
reviewingCommentReport = signal<CommentReportWithDetails | null>(null);
submitting = signal(false);
reviewForm = {
@@ -833,29 +1173,49 @@ export class AdminReportsComponent implements OnInit {
reviewNotes: ''
};
filteredReports = computed(() => {
filteredProfileReports = computed(() => {
const filter = this.activeFilter();
if (filter === 'all') {
return this.allReports();
return this.allProfileReports();
}
return this.allReports().filter(report => report.status === filter);
return this.allProfileReports().filter(report => report.status === filter);
});
pendingCount = computed(() =>
this.allReports().filter(r => r.status === ReportStatus.PENDING).length
);
filteredCommentReports = computed(() => {
const filter = this.activeFilter();
if (filter === 'all') {
return this.allCommentReports();
}
return this.allCommentReports().filter(report => report.status === filter);
});
reviewedCount = computed(() =>
this.allReports().filter(r => r.status === ReportStatus.REVIEWED).length
);
pendingCount = computed(() => {
if (this.reportType() === 'profile') {
return this.allProfileReports().filter(r => r.status === ReportStatus.PENDING).length;
}
return this.allCommentReports().filter(r => r.status === ReportStatus.PENDING).length;
});
dismissedCount = computed(() =>
this.allReports().filter(r => r.status === ReportStatus.DISMISSED).length
);
reviewedCount = computed(() => {
if (this.reportType() === 'profile') {
return this.allProfileReports().filter(r => r.status === ReportStatus.REVIEWED).length;
}
return this.allCommentReports().filter(r => r.status === ReportStatus.REVIEWED).length;
});
actionTakenCount = computed(() =>
this.allReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length
);
dismissedCount = computed(() => {
if (this.reportType() === 'profile') {
return this.allProfileReports().filter(r => r.status === ReportStatus.DISMISSED).length;
}
return this.allCommentReports().filter(r => r.status === ReportStatus.DISMISSED).length;
});
actionTakenCount = computed(() => {
if (this.reportType() === 'profile') {
return this.allProfileReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length;
}
return this.allCommentReports().filter(r => r.status === ReportStatus.ACTION_TAKEN).length;
});
ngOnInit(): void {
if (!this.authService.isAdmin()) {
@@ -870,34 +1230,54 @@ export class AdminReportsComponent implements OnInit {
this.loading.set(true);
this.error.set(null);
this.reportService.getAllReports().subscribe({
next: (reports) => {
this.allReports.set(reports);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message ?? 'Failed to load reports');
this.loading.set(false);
this.toastService.error('Failed to load reports');
}
});
if (this.reportType() === 'profile') {
this.reportService.getAllReports().subscribe({
next: (reports) => {
this.allProfileReports.set(reports);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message ?? 'Failed to load profile reports');
this.loading.set(false);
this.toastService.error('Failed to load profile reports');
}
});
} else {
this.commentReportService.getAllReports().subscribe({
next: (reports) => {
this.allCommentReports.set(reports);
this.loading.set(false);
},
error: (err) => {
this.error.set(err.message ?? 'Failed to load comment reports');
this.loading.set(false);
this.toastService.error('Failed to load comment reports');
}
});
}
}
setReportType(type: 'profile' | 'comment'): void {
this.reportType.set(type);
this.activeFilter.set('all');
this.loadReports();
}
setFilter(filter: 'all' | ReportStatus): void {
this.activeFilter.set(filter);
}
openReviewModal(report: ProfileReportWithUsers): void {
this.reviewingReport.set(report);
openProfileReviewModal(report: ProfileReportWithUsers): void {
this.reviewingProfileReport.set(report);
this.reviewForm = {
status: report.status,
reviewNotes: report.reviewNotes || ''
};
}
closeReviewModal(): void {
closeProfileReviewModal(): void {
if (!this.submitting()) {
this.reviewingReport.set(null);
this.reviewingProfileReport.set(null);
this.reviewForm = {
status: ReportStatus.PENDING,
reviewNotes: ''
@@ -905,8 +1285,8 @@ export class AdminReportsComponent implements OnInit {
}
}
submitReview(): void {
const report = this.reviewingReport();
submitProfileReview(): void {
const report = this.reviewingProfileReport();
if (!report) {
return;
}
@@ -919,20 +1299,73 @@ export class AdminReportsComponent implements OnInit {
this.reviewForm.reviewNotes || undefined
).subscribe({
next: (updatedReport) => {
this.allReports.update(reports =>
this.allProfileReports.update(reports =>
reports.map(r => r.id === updatedReport.id ? updatedReport : r)
);
this.toastService.success('Report updated successfully');
this.toastService.success('Profile report updated successfully');
this.submitting.set(false);
this.closeReviewModal();
this.closeProfileReviewModal();
},
error: (err) => {
this.toastService.error(err.message ?? 'Failed to update report');
this.toastService.error(err.message ?? 'Failed to update profile report');
this.submitting.set(false);
}
});
}
openCommentReviewModal(report: CommentReportWithDetails): void {
this.reviewingCommentReport.set(report);
this.reviewForm = {
status: report.status,
reviewNotes: report.reviewNotes || ''
};
}
closeCommentReviewModal(): void {
if (!this.submitting()) {
this.reviewingCommentReport.set(null);
this.reviewForm = {
status: ReportStatus.PENDING,
reviewNotes: ''
};
}
}
submitCommentReview(): void {
const report = this.reviewingCommentReport();
if (!report) {
return;
}
this.submitting.set(true);
this.commentReportService.updateReport(report.id, {
status: this.reviewForm.status,
reviewNotes: this.reviewForm.reviewNotes || undefined
}).subscribe({
next: (updatedReport) => {
this.allCommentReports.update(reports =>
reports.map(r => r.id === updatedReport.id ? updatedReport : r)
);
this.toastService.success('Comment report updated successfully');
this.submitting.set(false);
this.closeCommentReviewModal();
},
error: (err) => {
this.toastService.error(err.message ?? 'Failed to update comment report');
this.submitting.set(false);
}
});
}
truncateComment(content: string, maxLength = 200): string {
const stripped = content.replace(/<[^>]*>/g, '');
if (stripped.length <= maxLength) {
return stripped;
}
return stripped.substring(0, maxLength) + '...';
}
formatStatus(status: ReportStatus): string {
const statusMap: Record<ReportStatus, string> = {
[ReportStatus.PENDING]: 'Pending',