/** * @copyright 2026 NHCarrigan * @license Naomi's Public License * @author Naomi Carrigan, Hikari */ import { Component, Input, Output, EventEmitter, OnInit, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Game, GameStatus, CreateGameDto, UpdateGameDto, Link } from '@library/shared-types'; @Component({ selector: 'app-game-form', standalone: true, imports: [CommonModule, FormsModule], template: `

{{ mode === 'add' ? 'Add New Game' : 'Edit Game' }}

@if (imagePreview()) {
Box art preview
} @if (imageError()) { {{ imageError() }} }
@for (tag of formData.tags; track tag; let i = $index) { {{ tag }} }
`, styles: [` .game-form { background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e5e7eb; margin-bottom: 2rem; } .game-form h3 { margin: 0 0 1.5rem 0; color: #1f2937; font-size: 1.5rem; } .form-group { margin-bottom: 1rem; } .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #374151; } .form-group input[type="text"], .form-group input[type="number"], .form-group input[type="date"], .form-group input[type="url"], .form-group select, .form-group textarea { width: 100%; padding: 0.625rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 1rem; font-family: inherit; } .form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #ff6b6b; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } .tags-input-container { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.625rem; border: 1px solid #d1d5db; border-radius: 4px; min-height: 44px; } .tags-input-container input { flex: 1; border: none; outline: none; font-size: 1rem; min-width: 150px; } .tag { background: #ff6b6b; color: white; padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; } .tag-remove { background: none; border: none; color: white; cursor: pointer; font-size: 1.25rem; line-height: 1; padding: 0; } .links-list { margin-bottom: 0.75rem; } .link-item { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem; background: #f3f4f6; border-radius: 4px; margin-bottom: 0.5rem; } .link-add-form { display: grid; grid-template-columns: 1fr 2fr auto; gap: 0.5rem; } .image-preview { margin-top: 0.75rem; display: flex; align-items: center; gap: 1rem; } .image-preview img { max-width: 200px; max-height: 200px; border-radius: 4px; border: 1px solid #e5e7eb; } .error-text { color: #dc2626; font-size: 0.875rem; display: block; margin-top: 0.5rem; } .form-actions { display: flex; gap: 0.75rem; margin-top: 1.5rem; } .btn { padding: 0.625rem 1.25rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; transition: all 0.3s; font-weight: 500; } .btn:hover { opacity: 0.9; } .btn-primary { background: #ff6b6b; color: white; } .btn-secondary { background: #6b7280; color: white; } .btn-danger { background: #ef4444; color: white; } .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.875rem; } `] }) export class GameFormComponent implements OnInit { @Input() mode: 'add' | 'edit' = 'add'; @Input() game?: Game; @Input() initialData?: Partial; @Output() formSubmit = new EventEmitter(); @Output() formCancel = new EventEmitter(); GameStatus = GameStatus; formData: Partial = { tags: [], links: [] }; timeHours = 0; timeMinutes = 0; tagInput = ''; linkTitle = ''; linkUrl = ''; imagePreview = signal(null); imageError = signal(null); ngOnInit() { if (this.mode === 'edit' && this.game) { this.formData = { title: this.game.title, platform: this.game.platform, status: this.game.status, dateStarted: this.game.dateStarted, dateFinished: this.game.dateFinished, rating: this.game.rating, notes: this.game.notes, coverImage: this.game.coverImage, tags: [...(this.game.tags || [])], links: [...(this.game.links || [])], series: this.game.series, seriesOrder: this.game.seriesOrder, timeSpent: this.game.timeSpent }; if (this.game.timeSpent) { this.timeHours = Math.floor(this.game.timeSpent / 60); this.timeMinutes = this.game.timeSpent % 60; } this.imagePreview.set(this.game.coverImage || null); } else { // Add mode - use initialData if provided, otherwise defaults this.formData = { status: GameStatus.backlog, tags: [], links: [], ...this.initialData }; if (this.initialData?.coverImage) { this.imagePreview.set(this.initialData.coverImage); } } } updateTimeSpent() { this.formData.timeSpent = (this.timeHours * 60) + this.timeMinutes; } addTag() { if (this.tagInput.trim() && !this.formData.tags?.includes(this.tagInput.trim())) { this.formData.tags = [...(this.formData.tags || []), this.tagInput.trim()]; this.tagInput = ''; } } removeTag(index: number) { this.formData.tags = this.formData.tags?.filter((_, i) => i !== index) || []; } addLink() { if (this.linkTitle.trim() && this.linkUrl.trim()) { const newLink: Link = { title: this.linkTitle.trim(), url: this.linkUrl.trim() }; this.formData.links = [...(this.formData.links || []), newLink]; this.linkTitle = ''; this.linkUrl = ''; } } removeLink(index: number) { this.formData.links = this.formData.links?.filter((_, i) => i !== index) || []; } onImageSelected(event: Event) { const input = event.target as HTMLInputElement; const file = input.files?.[0]; if (!file) { return; } if (file.size > 500000) { this.imageError.set('Image must be under 500KB'); input.value = ''; return; } this.imageError.set(null); const reader = new FileReader(); reader.onload = () => { const base64String = reader.result as string; this.formData.coverImage = base64String; this.imagePreview.set(base64String); }; reader.readAsDataURL(file); } clearImage() { this.formData.coverImage = undefined; this.imagePreview.set(null); } onSubmit() { if (!this.formData.title || !this.formData.status) { return; } const data: CreateGameDto | UpdateGameDto = { ...this.formData as CreateGameDto | UpdateGameDto, dateStarted: this.formData.dateStarted ? new Date(this.formData.dateStarted) : undefined, dateFinished: this.formData.dateFinished ? new Date(this.formData.dateFinished) : undefined }; this.formSubmit.emit(data); } onCancel() { this.formCancel.emit(); } }