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>
95 lines
8.3 KiB
Markdown
95 lines
8.3 KiB
Markdown
# 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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.
|
||
|
||
- [X] 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
|
||
|
||
- [X] 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. T001–T005 — toast infrastructure
|
||
2. T006–T007 — 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.
|