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>
8.3 KiB
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.tscovering: (1)show()emits aToastobject oncurrent$with the correctmessageandtype; (2) after the duration elapses,current$emitsnull; (3)typedefaults to'success'when not provided; (4) callingshow()a second time before the first timer fires replaces the active toast. Runng testand confirm new tests FAIL. -
T002 [US2] Create
ui/src/app/services/toast.service.ts: (a) defineexport interface Toast { message: string; type: 'success' | 'error'; }; (b)@Injectable({ providedIn: 'root' })class with a privateBehaviorSubject<Toast | null>(null); (c) exposereadonly current$: Observable<Toast | null>from the subject; (d) implementshow(message: string, type: 'success' | 'error' = 'success', duration = 3000): void— emits the toast immediately, then callssetTimeout(() => this.subject.next(null), duration)(store the timer handle andclearTimeoutit at the start ofshow()so rapid calls replace correctly). Runng testand confirm T001 tests pass. -
T003 [P] [US2] Write tests in
ui/src/app/toast/toast.component.spec.tscovering: (1) whenToastService.current$emits a{ message: 'Done', type: 'success' }toast, a.toastelement is rendered containing "Done"; (2) the element has the CSS classsuccess; (3) when type is'error', the element has classerror; (4) whencurrent$emitsnull, no.toastelement is present. Runng testand 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) injectToastServiceas 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); }. Runng testand confirm T003 tests pass. -
T005 [US2] Register
ToastComponentinui/src/app/app.component.ts: addToastComponentto theimportsarray; add<app-toast></app-toast>to the template after<router-outlet />; add a test toui/src/app/app.component.spec.tsasserting that anapp-toastelement is present in the rendered output. Confirm all existing AppComponent tests still pass andng build --configuration developmentsucceeds.
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.tscovering: (1) a "Copy URL" button (.copy-url-btn) is present in the DOM when an image is loaded; (2) clicking it callsnavigator.clipboard.writeTextwith the image'sfile_urlwhenfile_urlis already absolute (starts withhttp); (3) whenfile_urlis relative (starts with/),writeTextis called withwindow.location.origin + file_url; (4) whenwriteTextresolves,toastService.showis called with a success message; (5) whenwriteTextrejects,toastService.showis called with an error message and type'error'. Spy onnavigator.clipboard.writeTextusingspyOn(navigator.clipboard, 'writeText')returningPromise.resolve()/Promise.reject()as appropriate. Runng testand confirm new tests FAIL. -
T007 [US1] Update
ui/src/app/detail/detail.component.ts: (a) injectToastService(add to constructor); (b) addcopyUrl(): voidmethod — resolves the URL asthis.image!.file_url.startsWith('http') ? this.image!.file_url : window.location.origin + this.image!.file_url, then callsnavigator.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 hoverborder-color: var(--border-focus). Runng testand 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 linton all modified and created files inui/src/app/; fix any issues. Confirmng testpasses with all new and existing tests green. Manually verify allquickstart.mdscenarios 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)
- T001–T005 — toast infrastructure
- T006–T007 — copy URL button
- STOP and VALIDATE: open browser, click Copy URL, confirm toast, paste to verify URL
- T008 — polish
- 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.