feat: add tags and links

This commit is contained in:
2026-02-04 19:49:27 -08:00
parent 9902c5ad45
commit b9f33bc055
21 changed files with 1873 additions and 31 deletions
@@ -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 { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@library/shared-types';
import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from '@library/shared-types';
@Component({
selector: 'app-art-gallery',
@@ -102,6 +102,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
</div>
}
<div class="form-group">
<label>Tags</label>
<div class="tags-input-container">
@for (tag of newArt.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 newArt.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., DeviantArt)"
>
<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 Artwork</button>
<button type="button" (click)="toggleAddForm()" class="btn btn-secondary">Cancel</button>
@@ -231,6 +277,52 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
</div>
}
<div class="form-group">
<label>Tags</label>
<div class="tags-input-container">
@for (tag of editArt.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 editArt.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., DeviantArt)"
>
<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>
@@ -262,6 +354,24 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
<p class="artist">by {{ art.artist }}</p>
<p class="date-added">Added: {{ formatDate(art.dateAdded) }}</p>
@if (art.tags && art.tags.length > 0) {
<div class="tags-display">
@for (tag of art.tags; track tag) {
<span class="tag-chip">{{ tag }}</span>
}
</div>
}
@if (art.links && art.links.length > 0) {
<div class="links-display">
@for (link of art.links; track link.url) {
<a [href]="link.url" target="_blank" rel="noopener noreferrer" class="external-link">
{{ link.title }} ↗
</a>
}
</div>
}
@if (authService.isAdmin()) {
<div class="actions">
<button (click)="startEdit(art)" class="btn btn-secondary btn-sm">
@@ -812,6 +922,116 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity } from '@lib
gap: 0.5rem;
margin-top: 0.5rem;
}
.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: #fdcb6e;
color: #333;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.85rem;
}
.tag-remove {
background: none;
border: none;
color: #333;
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(253, 203, 110, 0.2);
color: #b38f00;
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: #b38f00;
text-decoration: none;
font-size: 0.85rem;
padding: 0.2rem 0.5rem;
background: rgba(253, 203, 110, 0.1);
border-radius: 4px;
transition: background 0.2s;
}
.external-link:hover {
background: rgba(253, 203, 110, 0.2);
text-decoration: underline;
}
`]
})
export class ArtGalleryComponent implements OnInit {
@@ -848,11 +1068,21 @@ export class ArtGalleryComponent implements OnInit {
title: '',
artist: '',
imageUrl: '',
description: ''
description: '',
tags: [],
links: []
};
editArt: Partial<UpdateArtDto> = {};
// Tags and links input state
newTagInput = '';
editTagInput = '';
newLinkTitle = '';
newLinkUrl = '';
editLinkTitle = '';
editLinkUrl = '';
ngOnInit() {
this.loadArt();
}
@@ -882,8 +1112,58 @@ export class ArtGalleryComponent implements OnInit {
title: '',
artist: '',
imageUrl: '',
description: ''
description: '',
tags: [],
links: []
};
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.newArt.tags = [...(this.newArt.tags || []), input];
this.newTagInput = '';
} else {
this.editArt.tags = [...(this.editArt.tags || []), input];
this.editTagInput = '';
}
}
removeTag(index: number, target: 'new' | 'edit') {
if (target === 'new') {
this.newArt.tags = (this.newArt.tags || []).filter((_, i) => i !== index);
} else {
this.editArt.tags = (this.editArt.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.newArt.links = [...(this.newArt.links || []), { title, url }];
this.newLinkTitle = '';
this.newLinkUrl = '';
} else {
this.editArt.links = [...(this.editArt.links || []), { title, url }];
this.editLinkTitle = '';
this.editLinkUrl = '';
}
}
removeLink(index: number, target: 'new' | 'edit') {
if (target === 'new') {
this.newArt.links = (this.newArt.links || []).filter((_, i) => i !== index);
} else {
this.editArt.links = (this.editArt.links || []).filter((_, i) => i !== index);
}
}
addArt() {
@@ -893,7 +1173,9 @@ export class ArtGalleryComponent implements OnInit {
title: this.newArt.title,
artist: this.newArt.artist,
imageUrl: this.newArt.imageUrl,
description: this.newArt.description
description: this.newArt.description,
tags: this.newArt.tags || [],
links: this.newArt.links || []
};
this.artService.createArt(artToAdd).subscribe(() => {
@@ -916,14 +1198,22 @@ export class ArtGalleryComponent implements OnInit {
title: art.title,
artist: art.artist,
imageUrl: art.imageUrl,
description: art.description
description: art.description,
tags: [...(art.tags || [])],
links: [...(art.links || [])]
};
this.showAddForm.set(false);
this.editTagInput = '';
this.editLinkTitle = '';
this.editLinkUrl = '';
}
cancelEdit() {
this.editingArt.set(null);
this.editArt = {};
this.editTagInput = '';
this.editLinkTitle = '';
this.editLinkUrl = '';
}
saveEdit() {