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>
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
`,
|
||||
styles: [`
|
||||
@@ -1137,6 +1332,29 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR
|
||||
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 {
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
@@ -1256,6 +1474,7 @@ export class AdminReportsComponent implements OnInit {
|
||||
|
||||
reviewingProfileReport = signal<ProfileReportWithUsers | null>(null);
|
||||
reviewingCommentReport = signal<CommentReportWithDetails | null>(null);
|
||||
editingProfile = signal<{ userId: string; username: string; profile: any } | null>(null);
|
||||
submitting = signal(false);
|
||||
|
||||
reviewForm = {
|
||||
@@ -1263,6 +1482,20 @@ export class AdminReportsComponent implements OnInit {
|
||||
reviewNotes: ''
|
||||
};
|
||||
|
||||
profileEditForm = {
|
||||
displayName: '',
|
||||
slug: '',
|
||||
bio: '',
|
||||
profilePublic: true,
|
||||
website: '',
|
||||
discordServer: '',
|
||||
bluesky: '',
|
||||
github: '',
|
||||
linkedin: '',
|
||||
twitch: '',
|
||||
youtube: ''
|
||||
};
|
||||
|
||||
filteredProfileReports = computed(() => {
|
||||
const filter = this.activeFilter();
|
||||
if (filter === 'all') {
|
||||
@@ -1534,20 +1767,23 @@ export class AdminReportsComponent implements OnInit {
|
||||
// Load the full profile first to get current values
|
||||
this.userService.getProfile(userId).subscribe({
|
||||
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) {
|
||||
this.userService.adminUpdateUser(userId, { bio: newBio }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Profile updated successfully');
|
||||
this.loadReports();
|
||||
this.closeProfileReviewModal();
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.error(err.message ?? 'Failed to update profile');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Open the edit modal
|
||||
this.editingProfile.set({ userId, username, profile });
|
||||
},
|
||||
error: (err) => {
|
||||
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 {
|
||||
const userId = report.reportedUser.id;
|
||||
const username = report.reportedUser.username;
|
||||
|
||||
Reference in New Issue
Block a user