generated from nhcarrigan/template
feat: add tags and links
This commit is contained in:
@@ -12,7 +12,7 @@ import { AuthService } from '../../services/auth.service';
|
||||
import { CommentsService } from '../../services/comments.service';
|
||||
import { SanitizeService } from '../../services/sanitize.service';
|
||||
import { SuggestionService } from '../../services/suggestion.service';
|
||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity } from '@library/shared-types';
|
||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
|
||||
|
||||
@Component({
|
||||
selector: 'app-music-list',
|
||||
@@ -121,6 +121,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Tags</label>
|
||||
<div class="tags-input-container">
|
||||
@for (tag of newMusic.tags; track tag; let i = $index) {
|
||||
<span class="tag">
|
||||
{{ tag }}
|
||||
<button type="button" (click)="removeTag(i, 'new')" class="tag-remove">×</button>
|
||||
</span>
|
||||
}
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newTagInput"
|
||||
name="newTagInput"
|
||||
placeholder="Add a tag and press Enter"
|
||||
(keydown.enter)="addTag('new'); $event.preventDefault()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>External Links</label>
|
||||
<div class="links-list">
|
||||
@for (link of newMusic.links; track link.url; let i = $index) {
|
||||
<div class="link-item">
|
||||
<span>{{ link.title }}: {{ link.url }}</span>
|
||||
<button type="button" (click)="removeLink(i, 'new')" class="btn btn-danger btn-sm">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="link-add-form">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="newLinkTitle"
|
||||
name="newLinkTitle"
|
||||
placeholder="Link title (e.g., Spotify)"
|
||||
>
|
||||
<input
|
||||
type="url"
|
||||
[(ngModel)]="newLinkUrl"
|
||||
name="newLinkUrl"
|
||||
placeholder="https://..."
|
||||
>
|
||||
<button type="button" (click)="addLink('new')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Add Music</button>
|
||||
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
|
||||
@@ -216,6 +262,52 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Tags</label>
|
||||
<div class="tags-input-container">
|
||||
@for (tag of editMusicData.tags; track tag; let i = $index) {
|
||||
<span class="tag">
|
||||
{{ tag }}
|
||||
<button type="button" (click)="removeTag(i, 'edit')" class="tag-remove">×</button>
|
||||
</span>
|
||||
}
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="editTagInput"
|
||||
name="editTagInput"
|
||||
placeholder="Add a tag and press Enter"
|
||||
(keydown.enter)="addTag('edit'); $event.preventDefault()"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>External Links</label>
|
||||
<div class="links-list">
|
||||
@for (link of editMusicData.links; track link.url; let i = $index) {
|
||||
<div class="link-item">
|
||||
<span>{{ link.title }}: {{ link.url }}</span>
|
||||
<button type="button" (click)="removeLink(i, 'edit')" class="btn btn-danger btn-sm">×</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="link-add-form">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="editLinkTitle"
|
||||
name="editLinkTitle"
|
||||
placeholder="Link title (e.g., Spotify)"
|
||||
>
|
||||
<input
|
||||
type="url"
|
||||
[(ngModel)]="editLinkUrl"
|
||||
name="editLinkUrl"
|
||||
placeholder="https://..."
|
||||
>
|
||||
<button type="button" (click)="addLink('edit')" class="btn btn-secondary btn-sm">Add Link</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||
@@ -411,6 +503,24 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
||||
<p class="notes">{{ music.notes }}</p>
|
||||
}
|
||||
|
||||
@if (music.tags && music.tags.length > 0) {
|
||||
<div class="tags-display">
|
||||
@for (tag of music.tags; track tag) {
|
||||
<span class="tag-chip">{{ tag }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (music.links && music.links.length > 0) {
|
||||
<div class="links-display">
|
||||
@for (link of music.links; track link.url) {
|
||||
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||
{{ link.title }} ↗
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (music.dateCompleted) {
|
||||
<p class="date-completed">
|
||||
Completed: {{ formatDate(music.dateCompleted) }}
|
||||
@@ -1008,6 +1118,116 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
||||
input[type="file"]:hover {
|
||||
border-color: var(--witch-rose);
|
||||
}
|
||||
|
||||
.tags-input-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 2px solid var(--witch-lavender);
|
||||
border-radius: 4px;
|
||||
background: var(--witch-moon);
|
||||
}
|
||||
|
||||
.tags-input-container input {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tags-input-container input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: #74b9ff;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.tag-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tag-remove:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tags-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
background: rgba(116, 185, 255, 0.2);
|
||||
color: #74b9ff;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.links-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background: var(--witch-moon);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.link-add-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.link-add-form input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.links-display {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.external-link {
|
||||
color: #74b9ff;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background: rgba(116, 185, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.external-link:hover {
|
||||
background: rgba(116, 185, 255, 0.2);
|
||||
text-decoration: underline;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class MusicListComponent implements OnInit {
|
||||
@@ -1085,11 +1305,21 @@ export class MusicListComponent implements OnInit {
|
||||
type: MusicType.album,
|
||||
status: MusicStatus.wantToListen,
|
||||
rating: undefined,
|
||||
notes: ''
|
||||
notes: '',
|
||||
tags: [],
|
||||
links: []
|
||||
};
|
||||
|
||||
editMusicData: Partial<UpdateMusicDto> = {};
|
||||
|
||||
// Tags and links input state
|
||||
newTagInput = '';
|
||||
editTagInput = '';
|
||||
newLinkTitle = '';
|
||||
newLinkUrl = '';
|
||||
editLinkTitle = '';
|
||||
editLinkUrl = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.loadMusic();
|
||||
}
|
||||
@@ -1146,10 +1376,60 @@ export class MusicListComponent implements OnInit {
|
||||
status: MusicStatus.wantToListen,
|
||||
rating: undefined,
|
||||
notes: '',
|
||||
coverArt: undefined
|
||||
coverArt: undefined,
|
||||
tags: [],
|
||||
links: []
|
||||
};
|
||||
this.newMusicImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
this.newTagInput = '';
|
||||
this.newLinkTitle = '';
|
||||
this.newLinkUrl = '';
|
||||
}
|
||||
|
||||
addTag(target: 'new' | 'edit') {
|
||||
const input = target === 'new' ? this.newTagInput.trim() : this.editTagInput.trim();
|
||||
if (!input) return;
|
||||
|
||||
if (target === 'new') {
|
||||
this.newMusic.tags = [...(this.newMusic.tags || []), input];
|
||||
this.newTagInput = '';
|
||||
} else {
|
||||
this.editMusicData.tags = [...(this.editMusicData.tags || []), input];
|
||||
this.editTagInput = '';
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(index: number, target: 'new' | 'edit') {
|
||||
if (target === 'new') {
|
||||
this.newMusic.tags = (this.newMusic.tags || []).filter((_, i) => i !== index);
|
||||
} else {
|
||||
this.editMusicData.tags = (this.editMusicData.tags || []).filter((_, i) => i !== index);
|
||||
}
|
||||
}
|
||||
|
||||
addLink(target: 'new' | 'edit') {
|
||||
const title = target === 'new' ? this.newLinkTitle.trim() : this.editLinkTitle.trim();
|
||||
const url = target === 'new' ? this.newLinkUrl.trim() : this.editLinkUrl.trim();
|
||||
if (!title || !url) return;
|
||||
|
||||
if (target === 'new') {
|
||||
this.newMusic.links = [...(this.newMusic.links || []), { title, url }];
|
||||
this.newLinkTitle = '';
|
||||
this.newLinkUrl = '';
|
||||
} else {
|
||||
this.editMusicData.links = [...(this.editMusicData.links || []), { title, url }];
|
||||
this.editLinkTitle = '';
|
||||
this.editLinkUrl = '';
|
||||
}
|
||||
}
|
||||
|
||||
removeLink(index: number, target: 'new' | 'edit') {
|
||||
if (target === 'new') {
|
||||
this.newMusic.links = (this.newMusic.links || []).filter((_, i) => i !== index);
|
||||
} else {
|
||||
this.editMusicData.links = (this.editMusicData.links || []).filter((_, i) => i !== index);
|
||||
}
|
||||
}
|
||||
|
||||
addMusic() {
|
||||
@@ -1162,7 +1442,9 @@ export class MusicListComponent implements OnInit {
|
||||
status: this.newMusic.status,
|
||||
rating: this.newMusic.rating,
|
||||
notes: this.newMusic.notes,
|
||||
coverArt: this.newMusic.coverArt
|
||||
coverArt: this.newMusic.coverArt,
|
||||
tags: this.newMusic.tags || [],
|
||||
links: this.newMusic.links || []
|
||||
};
|
||||
|
||||
this.musicService.createMusic(musicToAdd).subscribe(() => {
|
||||
@@ -1188,11 +1470,16 @@ export class MusicListComponent implements OnInit {
|
||||
status: music.status,
|
||||
rating: music.rating,
|
||||
notes: music.notes,
|
||||
coverArt: music.coverArt
|
||||
coverArt: music.coverArt,
|
||||
tags: [...(music.tags || [])],
|
||||
links: [...(music.links || [])]
|
||||
};
|
||||
this.editMusicImagePreview.set(music.coverArt || null);
|
||||
this.showAddForm.set(false);
|
||||
this.imageError.set(null);
|
||||
this.editTagInput = '';
|
||||
this.editLinkTitle = '';
|
||||
this.editLinkUrl = '';
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
@@ -1200,6 +1487,9 @@ export class MusicListComponent implements OnInit {
|
||||
this.editMusicData = {};
|
||||
this.editMusicImagePreview.set(null);
|
||||
this.imageError.set(null);
|
||||
this.editTagInput = '';
|
||||
this.editLinkTitle = '';
|
||||
this.editLinkUrl = '';
|
||||
}
|
||||
|
||||
saveEdit() {
|
||||
|
||||
Reference in New Issue
Block a user