generated from nhcarrigan/template
feat: add suggestion feature
This commit is contained in:
@@ -11,7 +11,8 @@ import { BooksService } from '../../services/books.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { SanitizeService } from '../../services/sanitize.service';
|
||||
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@library/shared-types';
|
||||
import { SuggestionService } from '../../services/suggestion.service';
|
||||
import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEntity } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-books-list',
|
||||
@@ -25,6 +26,10 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
||||
<button (click)="toggleAddForm()" class="btn btn-primary">
|
||||
{{ showAddForm() ? 'Cancel' : 'Add Book' }}
|
||||
</button>
|
||||
} @else if (authService.isAuthenticated() && !authService.user()?.isBanned) {
|
||||
<button (click)="toggleSuggestForm()" class="btn btn-primary">
|
||||
{{ showSuggestForm() ? 'Cancel' : 'Suggest a Book' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -222,6 +227,83 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (showSuggestForm() && !authService.isAdmin() && authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="submitSuggestion()" class="add-form suggest-form">
|
||||
<h3>Suggest a Book</h3>
|
||||
<p class="suggest-note">Your suggestion will be reviewed by Naomi. If accepted, it will be added to the reading list!</p>
|
||||
<div class="form-group">
|
||||
<label for="suggest-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="suggest-title"
|
||||
[(ngModel)]="suggestedBook.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter book title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="suggest-author">Author</label>
|
||||
<input
|
||||
type="text"
|
||||
id="suggest-author"
|
||||
[(ngModel)]="suggestedBook.author"
|
||||
name="author"
|
||||
required
|
||||
placeholder="Enter author name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="suggest-isbn">ISBN (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="suggest-isbn"
|
||||
[(ngModel)]="suggestedBook.isbn"
|
||||
name="isbn"
|
||||
placeholder="ISBN (optional)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="suggest-notes">Notes (why should Naomi read this?)</label>
|
||||
<textarea
|
||||
id="suggest-notes"
|
||||
[(ngModel)]="suggestedBook.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Tell Naomi why this book is worth reading..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="suggest-coverImage">Cover Image (max 500KB)</label>
|
||||
<input
|
||||
type="file"
|
||||
id="suggest-coverImage"
|
||||
name="coverImage"
|
||||
accept="image/*"
|
||||
(change)="onImageSelected($event, 'suggest')"
|
||||
>
|
||||
@if (suggestBookImagePreview()) {
|
||||
<div class="image-preview">
|
||||
<img [src]="suggestBookImagePreview()" alt="Cover 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">
|
||||
<button
|
||||
(click)="setFilter('all')"
|
||||
@@ -422,6 +504,18 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@librar
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.suggest-form {
|
||||
border: 2px solid #8b6f47;
|
||||
background: #faf8f5;
|
||||
}
|
||||
|
||||
.suggest-note {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -851,6 +945,7 @@ export class BooksListComponent implements OnInit {
|
||||
authService = inject(AuthService);
|
||||
commentsService = inject(CommentsService);
|
||||
sanitizeService = inject(SanitizeService);
|
||||
suggestionService = inject(SuggestionService);
|
||||
|
||||
books = signal<Book[]>([]);
|
||||
loading = signal(true);
|
||||
@@ -869,9 +964,20 @@ export class BooksListComponent implements OnInit {
|
||||
// Image upload state
|
||||
newBookImagePreview = signal<string | null>(null);
|
||||
editBookImagePreview = signal<string | null>(null);
|
||||
suggestBookImagePreview = signal<string | null>(null);
|
||||
imageError = signal<string | null>(null);
|
||||
private readonly MAX_IMAGE_SIZE = 500 * 1024; // 500KB
|
||||
|
||||
// Suggestion state
|
||||
showSuggestForm = signal(false);
|
||||
suggestedBook: { title: string; author: string; isbn?: string; notes?: string; coverImage?: string } = {
|
||||
title: '',
|
||||
author: '',
|
||||
isbn: '',
|
||||
notes: '',
|
||||
coverImage: undefined
|
||||
};
|
||||
|
||||
// Expose BookStatus enum to template
|
||||
BookStatus = BookStatus;
|
||||
|
||||
@@ -1014,7 +1120,7 @@ export class BooksListComponent 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];
|
||||
|
||||
@@ -1040,21 +1146,27 @@ export class BooksListComponent implements OnInit {
|
||||
if (target === 'new') {
|
||||
this.newBookImagePreview.set(base64);
|
||||
this.newBook.coverImage = base64;
|
||||
} else {
|
||||
} else if (target === 'edit') {
|
||||
this.editBookImagePreview.set(base64);
|
||||
this.editBook.coverImage = base64;
|
||||
} else {
|
||||
this.suggestBookImagePreview.set(base64);
|
||||
this.suggestedBook.coverImage = base64;
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
clearImage(target: 'new' | 'edit') {
|
||||
clearImage(target: 'new' | 'edit' | 'suggest') {
|
||||
if (target === 'new') {
|
||||
this.newBookImagePreview.set(null);
|
||||
this.newBook.coverImage = undefined;
|
||||
} else {
|
||||
} else if (target === 'edit') {
|
||||
this.editBookImagePreview.set(null);
|
||||
this.editBook.coverImage = undefined;
|
||||
} else {
|
||||
this.suggestBookImagePreview.set(null);
|
||||
this.suggestedBook.coverImage = undefined;
|
||||
}
|
||||
this.imageError.set(null);
|
||||
}
|
||||
@@ -1169,4 +1281,43 @@ export class BooksListComponent implements OnInit {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Suggestion methods
|
||||
toggleSuggestForm() {
|
||||
this.showSuggestForm.update(v => !v);
|
||||
if (!this.showSuggestForm()) {
|
||||
this.resetSuggestForm();
|
||||
}
|
||||
}
|
||||
|
||||
resetSuggestForm() {
|
||||
this.suggestedBook = {
|
||||
title: '',
|
||||
author: '',
|
||||
isbn: '',
|
||||
notes: '',
|
||||
coverImage: undefined
|
||||
};
|
||||
this.suggestBookImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
}
|
||||
|
||||
async submitSuggestion() {
|
||||
if (!this.suggestedBook.title || !this.suggestedBook.author) return;
|
||||
|
||||
try {
|
||||
await this.suggestionService.createSuggestion({
|
||||
entityType: SuggestionEntity.BOOK,
|
||||
title: this.suggestedBook.title,
|
||||
author: this.suggestedBook.author,
|
||||
isbn: this.suggestedBook.isbn,
|
||||
notes: this.suggestedBook.notes,
|
||||
coverImage: this.suggestedBook.coverImage
|
||||
});
|
||||
alert('Thank you for your suggestion! It will be reviewed soon.');
|
||||
this.toggleSuggestForm();
|
||||
} catch {
|
||||
alert('Failed to submit suggestion. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user