Files
agatha 7d49c12ce2 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>
2026-05-09 22:21:48 +00:00

8.3 KiB
Raw Permalink Blame History

Tasks: Copy URL & Toast Notifications

Input: Design documents from specs/016-copy-url-toast/ Prerequisites: plan.md , spec.md , research.md , contracts/toast-service.md , quickstart.md

Tests: Tests accompany each implementation task per §5.1. All changes are in ui/src/app/.

Organization: No project setup needed — Angular project exists. The toast infrastructure (US2) must be built before the copy URL feature (US1) can use it, so phases follow implementation dependency order rather than spec priority order.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to

Phase 1: Toast Infrastructure — User Story 2 (Foundational)

Goal: Build the reusable ToastService and ToastComponent that US1 and all future features depend on.

Independent Test: In the browser console, call toastService.show('Hello!') from any page — confirm a toast appears at the bottom of the screen for ~3 seconds then disappears. Confirm no interaction with the rest of the UI is blocked.

  • T001 [US2] Write tests in ui/src/app/services/toast.service.spec.ts covering: (1) show() emits a Toast object on current$ with the correct message and type; (2) after the duration elapses, current$ emits null; (3) type defaults to 'success' when not provided; (4) calling show() a second time before the first timer fires replaces the active toast. Run ng test and confirm new tests FAIL.

  • T002 [US2] Create ui/src/app/services/toast.service.ts: (a) define export interface Toast { message: string; type: 'success' | 'error'; }; (b) @Injectable({ providedIn: 'root' }) class with a private BehaviorSubject<Toast | null>(null); (c) expose readonly current$: Observable<Toast | null> from the subject; (d) implement show(message: string, type: 'success' | 'error' = 'success', duration = 3000): void — emits the toast immediately, then calls setTimeout(() => this.subject.next(null), duration) (store the timer handle and clearTimeout it at the start of show() so rapid calls replace correctly). Run ng test and confirm T001 tests pass.

  • T003 [P] [US2] Write tests in ui/src/app/toast/toast.component.spec.ts covering: (1) when ToastService.current$ emits a { message: 'Done', type: 'success' } toast, a .toast element is rendered containing "Done"; (2) the element has the CSS class success; (3) when type is 'error', the element has class error; (4) when current$ emits null, no .toast element is present. Run ng test and confirm new tests FAIL.

  • T004 [US2] Create ui/src/app/toast/toast.component.ts: (a) standalone component, selector: 'app-toast', ChangeDetectionStrategy.OnPush, imports [CommonModule]; (b) inject ToastService as public; (c) template: <div *ngIf="toastService.current$ | async as toast" class="toast" [class.success]="toast.type === 'success'" [class.error]="toast.type === 'error'">{{ toast.message }}</div>; (d) 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; } with .success { background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); } and .error { background: var(--danger); color: var(--danger-text); }. Run ng test and confirm T003 tests pass.

  • T005 [US2] Register ToastComponent in ui/src/app/app.component.ts: add ToastComponent to the imports array; add <app-toast></app-toast> to the template after <router-outlet />; add a test to ui/src/app/app.component.spec.ts asserting that an app-toast element is present in the rendered output. Confirm all existing AppComponent tests still pass and ng build --configuration development succeeds.

Checkpoint: Toast infrastructure complete. Any component in the app can now inject ToastService and call show().


Phase 2: Copy URL Button — User Story 1 (Priority: P1) 🎯 MVP

Goal: Add a "Copy URL" button to the image detail page. One click copies the direct image file URL to the clipboard and shows a confirmation toast.

Independent Test: Open any image detail page. Click "Copy URL". Confirm a success toast appears. Paste into a text editor and confirm the pasted value is the full image file URL. Then simulate a clipboard failure (e.g. revoke clipboard permission) and confirm an error toast appears instead.

  • T006 [US1] Write tests in ui/src/app/detail/detail.component.spec.ts covering: (1) a "Copy URL" button (.copy-url-btn) is present in the DOM when an image is loaded; (2) clicking it calls navigator.clipboard.writeText with the image's file_url when file_url is already absolute (starts with http); (3) when file_url is relative (starts with /), writeText is called with window.location.origin + file_url; (4) when writeText resolves, toastService.show is called with a success message; (5) when writeText rejects, toastService.show is called with an error message and type 'error'. Spy on navigator.clipboard.writeText using spyOn(navigator.clipboard, 'writeText') returning Promise.resolve() / Promise.reject() as appropriate. Run ng test and confirm new tests FAIL.

  • T007 [US1] Update ui/src/app/detail/detail.component.ts: (a) inject ToastService (add to constructor); (b) add copyUrl(): void method — resolves the URL as this.image!.file_url.startsWith('http') ? this.image!.file_url : window.location.origin + this.image!.file_url, then calls navigator.clipboard.writeText(url).then(() => this.toastService.show('URL copied!')).catch(() => this.toastService.show('Failed to copy URL', 'error')); (c) add a <button class="copy-url-btn" (click)="copyUrl()">Copy URL</button> to the template inside the *ngIf="image && !loading" block, placed below the image and above the tags section; (d) style: padding: 8px 20px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; margin: 12px 0; with hover border-color: var(--border-focus). Run ng test and confirm T006 tests pass.

Checkpoint: US1 complete. Detail page has a working Copy URL button with toast feedback.


Phase 3: Polish & Cross-Cutting Concerns

  • T008 Run ng lint on all modified and created files in ui/src/app/; fix any issues. Confirm ng test passes with all new and existing tests green. Manually verify all quickstart.md scenarios in a browser: happy path copy, auto-dismiss, clipboard error, rapid clicks, regression on other pages.

Dependencies & Execution Order

  • T001 before T002 (write failing tests before service implementation)
  • T002 before T003/T004 (service must exist for component tests to import it)
  • T003 before T004 (write failing tests before component implementation)
  • T004 before T005 (component must exist before registering in app)
  • T005 before T006 (toast infrastructure must be complete before copy URL tests)
  • T006 before T007 (write failing tests before detail component changes)
  • T007 before T008 (implementation before polish)

Execution Order Summary

Step 1: T001   (US2: failing ToastService tests)
Step 2: T002   (US2: ToastService implementation — tests turn green)
Step 3: T003   (US2: failing ToastComponent tests)  [can parallel with T002 if needed]
Step 4: T004   (US2: ToastComponent implementation — tests turn green)
Step 5: T005   (US2: wire ToastComponent into AppComponent)
Step 6: T006   (US1: failing copy URL tests)
Step 7: T007   (US1: copy URL implementation — tests turn green)
Step 8: T008   (Polish: lint + manual verification)

Implementation Strategy

MVP (US1 — single story delivers full feature value)

  1. T001T005 — toast infrastructure
  2. T006T007 — copy URL button
  3. STOP and VALIDATE: open browser, click Copy URL, confirm toast, paste to verify URL
  4. T008 — polish
  5. Deploy

Note on Priority Ordering

US2 (toast system) is listed as P2 in the spec because it is infrastructure rather than the end-user-visible feature. However it is a hard implementation prerequisite for US1 (P1). Phases follow implementation dependency order: US2 infrastructure is built first, US1 feature consumes it second.