generated from nhcarrigan/template
feat: add comprehensive profile editing modal for admins
Replaced the simple prompt-based profile editing with a full-featured modal that allows admins to edit all profile fields: Profile Information: - Display Name - Profile URL Slug (with uniqueness validation) - Bio (with character counter) Social Links: - Website - Discord Server - GitHub username - Bluesky handle - LinkedIn username - Twitch username - YouTube handle/channel All fields include: - Proper validation patterns - Help text explaining format requirements - Styled form sections for organization - Loading states during submission - Success/error toast notifications The modal opens when clicking "Edit Profile" on a profile report, loads the current profile data, and saves all changes via the admin API endpoint.
This commit is contained in:
@@ -598,6 +598,201 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Profile Edit Modal -->
|
||||||
|
@if (editingProfile()) {
|
||||||
|
<div
|
||||||
|
class="modal-overlay"
|
||||||
|
(click)="closeProfileEditModal()"
|
||||||
|
(keydown.escape)="closeProfileEditModal()"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="edit-profile-modal-title"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-content"
|
||||||
|
(click)="$event.stopPropagation()"
|
||||||
|
(keydown)="$event.stopPropagation()"
|
||||||
|
role="document"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="edit-profile-modal-title">Edit Profile: {{ editingProfile()!.username }}</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="modal-close"
|
||||||
|
(click)="closeProfileEditModal()"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form (ngSubmit)="saveProfileEdit()" class="profile-edit-form">
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Profile Information</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="displayName">Display Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="displayName"
|
||||||
|
name="displayName"
|
||||||
|
[(ngModel)]="profileEditForm.displayName"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Display name"
|
||||||
|
maxlength="50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slug">Profile URL Slug</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="slug"
|
||||||
|
name="slug"
|
||||||
|
[(ngModel)]="profileEditForm.slug"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="url-slug"
|
||||||
|
maxlength="30"
|
||||||
|
pattern="[a-z0-9-]+"
|
||||||
|
/>
|
||||||
|
<small class="form-help">Only lowercase letters, numbers, and hyphens</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bio">Bio</label>
|
||||||
|
<textarea
|
||||||
|
id="bio"
|
||||||
|
name="bio"
|
||||||
|
[(ngModel)]="profileEditForm.bio"
|
||||||
|
class="form-control"
|
||||||
|
rows="4"
|
||||||
|
maxlength="500"
|
||||||
|
placeholder="User bio..."
|
||||||
|
></textarea>
|
||||||
|
<small class="form-help">{{ (profileEditForm.bio.length || 0) }} / 500 characters</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h3>Social Links</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="website">Website</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="website"
|
||||||
|
name="website"
|
||||||
|
[(ngModel)]="profileEditForm.website"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
pattern="https?://.+"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="discordServer">Discord Server</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="discordServer"
|
||||||
|
name="discordServer"
|
||||||
|
[(ngModel)]="profileEditForm.discordServer"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="https://discord.gg/..."
|
||||||
|
pattern="https://discord\\.gg/[a-zA-Z0-9]+"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="github">GitHub</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="github"
|
||||||
|
name="github"
|
||||||
|
[(ngModel)]="profileEditForm.github"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="bluesky">Bluesky</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="bluesky"
|
||||||
|
name="bluesky"
|
||||||
|
[(ngModel)]="profileEditForm.bluesky"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username.bsky.social"
|
||||||
|
pattern="[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="linkedin">LinkedIn</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="linkedin"
|
||||||
|
name="linkedin"
|
||||||
|
[(ngModel)]="profileEditForm.linkedin"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9-]{3,100}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="twitch">Twitch</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="twitch"
|
||||||
|
name="twitch"
|
||||||
|
[(ngModel)]="profileEditForm.twitch"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username"
|
||||||
|
pattern="[a-zA-Z0-9_]{4,25}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="youtube">YouTube</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="youtube"
|
||||||
|
name="youtube"
|
||||||
|
[(ngModel)]="profileEditForm.youtube"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="@username or channel-id"
|
||||||
|
pattern="(@[a-zA-Z0-9_.-]{3,30}|UC[a-zA-Z0-9_-]{22})"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
(click)="closeProfileEditModal()"
|
||||||
|
[disabled]="submitting()"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
[disabled]="submitting()"
|
||||||
|
>
|
||||||
|
{{ submitting() ? 'Saving...' : 'Save Changes' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [`
|
||||||
@@ -1137,6 +1332,29 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-help {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--witch-mauve);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--witch-lavender);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h3 {
|
||||||
|
color: var(--witch-purple);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 2px solid var(--witch-lavender);
|
border: 2px solid var(--witch-lavender);
|
||||||
@@ -1256,6 +1474,7 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
|
|
||||||
reviewingProfileReport = signal<ProfileReportWithUsers | null>(null);
|
reviewingProfileReport = signal<ProfileReportWithUsers | null>(null);
|
||||||
reviewingCommentReport = signal<CommentReportWithDetails | null>(null);
|
reviewingCommentReport = signal<CommentReportWithDetails | null>(null);
|
||||||
|
editingProfile = signal<{ userId: string; username: string; profile: any } | null>(null);
|
||||||
submitting = signal(false);
|
submitting = signal(false);
|
||||||
|
|
||||||
reviewForm = {
|
reviewForm = {
|
||||||
@@ -1263,6 +1482,20 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
reviewNotes: ''
|
reviewNotes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
profileEditForm = {
|
||||||
|
displayName: '',
|
||||||
|
slug: '',
|
||||||
|
bio: '',
|
||||||
|
profilePublic: true,
|
||||||
|
website: '',
|
||||||
|
discordServer: '',
|
||||||
|
bluesky: '',
|
||||||
|
github: '',
|
||||||
|
linkedin: '',
|
||||||
|
twitch: '',
|
||||||
|
youtube: ''
|
||||||
|
};
|
||||||
|
|
||||||
filteredProfileReports = computed(() => {
|
filteredProfileReports = computed(() => {
|
||||||
const filter = this.activeFilter();
|
const filter = this.activeFilter();
|
||||||
if (filter === 'all') {
|
if (filter === 'all') {
|
||||||
@@ -1534,20 +1767,23 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
// Load the full profile first to get current values
|
// Load the full profile first to get current values
|
||||||
this.userService.getProfile(userId).subscribe({
|
this.userService.getProfile(userId).subscribe({
|
||||||
next: (profile) => {
|
next: (profile) => {
|
||||||
const newBio = prompt(`Edit bio for ${username}:`, profile.bio || '');
|
// Populate the edit form with current values
|
||||||
|
this.profileEditForm = {
|
||||||
|
displayName: profile.displayName || '',
|
||||||
|
slug: profile.slug || '',
|
||||||
|
bio: profile.bio || '',
|
||||||
|
profilePublic: true, // We'll get this from the full user object if needed
|
||||||
|
website: profile.website || '',
|
||||||
|
discordServer: profile.discordServer || '',
|
||||||
|
bluesky: profile.bluesky || '',
|
||||||
|
github: profile.github || '',
|
||||||
|
linkedin: profile.linkedin || '',
|
||||||
|
twitch: profile.twitch || '',
|
||||||
|
youtube: profile.youtube || ''
|
||||||
|
};
|
||||||
|
|
||||||
if (newBio !== null) {
|
// Open the edit modal
|
||||||
this.userService.adminUpdateUser(userId, { bio: newBio }).subscribe({
|
this.editingProfile.set({ userId, username, profile });
|
||||||
next: () => {
|
|
||||||
this.toastService.success('Profile updated successfully');
|
|
||||||
this.loadReports();
|
|
||||||
this.closeProfileReviewModal();
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
this.toastService.error(err.message ?? 'Failed to update profile');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.toastService.error(err.message ?? 'Failed to load profile');
|
this.toastService.error(err.message ?? 'Failed to load profile');
|
||||||
@@ -1555,6 +1791,43 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeProfileEditModal(): void {
|
||||||
|
this.editingProfile.set(null);
|
||||||
|
this.profileEditForm = {
|
||||||
|
displayName: '',
|
||||||
|
slug: '',
|
||||||
|
bio: '',
|
||||||
|
profilePublic: true,
|
||||||
|
website: '',
|
||||||
|
discordServer: '',
|
||||||
|
bluesky: '',
|
||||||
|
github: '',
|
||||||
|
linkedin: '',
|
||||||
|
twitch: '',
|
||||||
|
youtube: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProfileEdit(): void {
|
||||||
|
const editing = this.editingProfile();
|
||||||
|
if (!editing) return;
|
||||||
|
|
||||||
|
this.submitting.set(true);
|
||||||
|
this.userService.adminUpdateUser(editing.userId, this.profileEditForm).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toastService.success('Profile updated successfully');
|
||||||
|
this.loadReports();
|
||||||
|
this.closeProfileEditModal();
|
||||||
|
this.closeProfileReviewModal();
|
||||||
|
this.submitting.set(false);
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.toastService.error(err.message ?? 'Failed to update profile');
|
||||||
|
this.submitting.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
makeProfilePrivate(report: ProfileReportWithUsers): void {
|
makeProfilePrivate(report: ProfileReportWithUsers): void {
|
||||||
const userId = report.reportedUser.id;
|
const userId = report.reportedUser.id;
|
||||||
const username = report.reportedUser.username;
|
const username = report.reportedUser.username;
|
||||||
|
|||||||
Reference in New Issue
Block a user