Feat: Add Copy URL button and reusable toast notification system
Detail page now has a "Copy URL" button that copies the image's direct file URL to the clipboard. A toast service (BehaviorSubject-backed, auto-dismissing after 3s) confirms success or failure. ToastComponent is registered at the app root and available to all future features. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
49
ui/src/app/toast/toast.component.spec.ts
Normal file
49
ui/src/app/toast/toast.component.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { ToastComponent } from './toast.component';
|
||||
import { ToastService } from '../services/toast.service';
|
||||
|
||||
describe('ToastComponent', () => {
|
||||
let toastSvc: jasmine.SpyObj<ToastService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
toastSvc = jasmine.createSpyObj('ToastService', [], { current$: of(null) });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ToastComponent],
|
||||
providers: [{ provide: ToastService, useValue: toastSvc }],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('renders a .toast element with the correct message when current$ emits a toast', async () => {
|
||||
Object.defineProperty(toastSvc, 'current$', { value: of({ message: 'Done', type: 'success' as const }) });
|
||||
const fixture = TestBed.createComponent(ToastComponent);
|
||||
fixture.detectChanges();
|
||||
const el = (fixture.nativeElement as HTMLElement).querySelector('.toast');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el?.textContent?.trim()).toBe('Done');
|
||||
});
|
||||
|
||||
it('adds the success CSS class when type is success', async () => {
|
||||
Object.defineProperty(toastSvc, 'current$', { value: of({ message: 'OK', type: 'success' as const }) });
|
||||
const fixture = TestBed.createComponent(ToastComponent);
|
||||
fixture.detectChanges();
|
||||
const el = (fixture.nativeElement as HTMLElement).querySelector('.toast');
|
||||
expect(el?.classList.contains('success')).toBeTrue();
|
||||
});
|
||||
|
||||
it('adds the error CSS class when type is error', async () => {
|
||||
Object.defineProperty(toastSvc, 'current$', { value: of({ message: 'Fail', type: 'error' as const }) });
|
||||
const fixture = TestBed.createComponent(ToastComponent);
|
||||
fixture.detectChanges();
|
||||
const el = (fixture.nativeElement as HTMLElement).querySelector('.toast');
|
||||
expect(el?.classList.contains('error')).toBeTrue();
|
||||
});
|
||||
|
||||
it('renders nothing when current$ emits null', () => {
|
||||
const fixture = TestBed.createComponent(ToastComponent);
|
||||
fixture.detectChanges();
|
||||
const el = (fixture.nativeElement as HTMLElement).querySelector('.toast');
|
||||
expect(el).toBeNull();
|
||||
});
|
||||
});
|
||||
44
ui/src/app/toast/toast.component.ts
Normal file
44
ui/src/app/toast/toast.component.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ToastService } from '../services/toast.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toast',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div
|
||||
*ngIf="toastService.current$ | async as toast"
|
||||
class="toast"
|
||||
[class.success]="toast.type === 'success'"
|
||||
[class.error]="toast.type === 'error'"
|
||||
>{{ toast.message }}</div>
|
||||
`,
|
||||
styles: [`
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.9rem;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.success {
|
||||
background: var(--surface-raised);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.error {
|
||||
background: var(--danger);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class ToastComponent {
|
||||
constructor(public toastService: ToastService) {}
|
||||
}
|
||||
Reference in New Issue
Block a user