feat: add suggestion feature

This commit is contained in:
2026-02-04 19:09:28 -08:00
parent 912a8887a5
commit 9902c5ad45
22 changed files with 2666 additions and 49 deletions
@@ -11,7 +11,8 @@ import { MusicService } from '../../services/music.service';
import { AuthService } from '../../services/auth.service';
import { CommentsService } from '../../services/comments.service';
import { SanitizeService } from '../../services/sanitize.service';
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment } from '@library/shared-types';
import { SuggestionService } from '../../services/suggestion.service';
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity } from '@library/shared-types';
@Component({
selector: 'app-music-list',
@@ -25,6 +26,10 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
<button (click)="toggleAddForm()" class="btn btn-primary">
{{ showAddForm() ? 'Cancel' : 'Add Music' }}
</button>
} @else if (authService.isAuthenticated() && !authService.user()?.isBanned) {
<button (click)="toggleSuggestForm()" class="btn btn-primary">
{{ showSuggestForm() ? 'Cancel' : 'Suggest Music' }}
</button>
}
</div>
@@ -218,6 +223,81 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
</form>
}
@if (showSuggestForm() && !authService.isAdmin() && authService.isAuthenticated()) {
<form (ngSubmit)="submitSuggestion()" class="add-form suggest-form">
<h3>Suggest Music</h3>
<p class="suggest-note">Your suggestion will be reviewed by Naomi. If accepted, it will be added to the listening list!</p>
<div class="form-group">
<label for="suggest-title">Title</label>
<input
type="text"
id="suggest-title"
[(ngModel)]="suggestedMusic.title"
name="title"
required
placeholder="Album/Single/EP title"
>
</div>
<div class="form-group">
<label for="suggest-artist">Artist</label>
<input
type="text"
id="suggest-artist"
[(ngModel)]="suggestedMusic.artist"
name="artist"
required
placeholder="Artist name"
>
</div>
<div class="form-group">
<label for="suggest-type">Type</label>
<select id="suggest-type" [(ngModel)]="suggestedMusic.type" name="type" required>
<option [value]="MusicType.album">Album</option>
<option [value]="MusicType.single">Single</option>
<option [value]="MusicType.ep">EP</option>
</select>
</div>
<div class="form-group">
<label for="suggest-notes">Notes (why should Naomi listen to this?)</label>
<textarea
id="suggest-notes"
[(ngModel)]="suggestedMusic.notes"
name="notes"
rows="3"
placeholder="Tell Naomi why this music is worth listening to..."
></textarea>
</div>
<div class="form-group">
<label for="suggest-coverArt">Album Art (max 500KB)</label>
<input
type="file"
id="suggest-coverArt"
name="coverArt"
accept="image/*"
(change)="onImageSelected($event, 'suggest')"
>
@if (suggestMusicImagePreview()) {
<div class="image-preview">
<img [src]="suggestMusicImagePreview()" alt="Album art preview">
<button type="button" (click)="clearImage('suggest')" class="btn btn-danger btn-sm">Remove</button>
</div>
}
@if (imageError()) {
<span class="error-text">{{ imageError() }}</span>
}
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Submit Suggestion</button>
<button type="button" (click)="toggleSuggestForm()" class="btn btn-secondary">Cancel</button>
</div>
</form>
}
<div class="filters">
<div class="filter-group">
<strong>Type:</strong>
@@ -460,6 +540,18 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment
backdrop-filter: blur(10px);
}
.suggest-form {
border: 2px solid #74b9ff;
background: #f5faff;
}
.suggest-note {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
font-style: italic;
}
.form-group {
margin-bottom: 1rem;
}
@@ -923,6 +1015,7 @@ export class MusicListComponent implements OnInit {
authService = inject(AuthService);
commentsService = inject(CommentsService);
sanitizeService = inject(SanitizeService);
suggestionService = inject(SuggestionService);
music = signal<Music[]>([]);
loading = signal(true);
@@ -942,9 +1035,20 @@ export class MusicListComponent implements OnInit {
// Image upload state
newMusicImagePreview = signal<string | null>(null);
editMusicImagePreview = signal<string | null>(null);
suggestMusicImagePreview = signal<string | null>(null);
imageError = signal<string | null>(null);
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
// Suggestion state
showSuggestForm = signal(false);
suggestedMusic: { title: string; artist: string; type: MusicType; notes?: string; coverArt?: string } = {
title: '',
artist: '',
type: MusicType.album,
notes: '',
coverArt: undefined
};
// Expose enums to template
MusicType = MusicType;
MusicStatus = MusicStatus;
@@ -1113,7 +1217,7 @@ export class MusicListComponent implements OnInit {
}
// Image handling methods
onImageSelected(event: Event, target: 'new' | 'edit') {
onImageSelected(event: Event, target: 'new' | 'edit' | 'suggest') {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
@@ -1139,21 +1243,27 @@ export class MusicListComponent implements OnInit {
if (target === 'new') {
this.newMusicImagePreview.set(base64);
this.newMusic.coverArt = base64;
} else {
} else if (target === 'edit') {
this.editMusicImagePreview.set(base64);
this.editMusicData.coverArt = base64;
} else {
this.suggestMusicImagePreview.set(base64);
this.suggestedMusic.coverArt = base64;
}
};
reader.readAsDataURL(file);
}
clearImage(target: 'new' | 'edit') {
clearImage(target: 'new' | 'edit' | 'suggest') {
if (target === 'new') {
this.newMusicImagePreview.set(null);
this.newMusic.coverArt = undefined;
} else {
} else if (target === 'edit') {
this.editMusicImagePreview.set(null);
this.editMusicData.coverArt = undefined;
} else {
this.suggestMusicImagePreview.set(null);
this.suggestedMusic.coverArt = undefined;
}
this.imageError.set(null);
}
@@ -1268,4 +1378,43 @@ export class MusicListComponent implements OnInit {
}
});
}
// Suggestion methods
toggleSuggestForm() {
this.showSuggestForm.update(v => !v);
if (!this.showSuggestForm()) {
this.resetSuggestForm();
}
}
resetSuggestForm() {
this.suggestedMusic = {
title: '',
artist: '',
type: MusicType.album,
notes: '',
coverArt: undefined
};
this.suggestMusicImagePreview.set(null);
this.imageError.set(null);
}
async submitSuggestion() {
if (!this.suggestedMusic.title || !this.suggestedMusic.artist) return;
try {
await this.suggestionService.createSuggestion({
entityType: SuggestionEntity.MUSIC,
title: this.suggestedMusic.title,
artist: this.suggestedMusic.artist,
type: this.suggestedMusic.type,
notes: this.suggestedMusic.notes,
coverArt: this.suggestedMusic.coverArt
});
alert('Thank you for your suggestion! It will be reviewed soon.');
this.toggleSuggestForm();
} catch {
alert('Failed to submit suggestion. Please try again.');
}
}
}