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:
55
specs/016-copy-url-toast/research.md
Normal file
55
specs/016-copy-url-toast/research.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Research: Copy URL & Toast Notifications
|
||||
|
||||
## Decision 1: Toast Service Architecture
|
||||
|
||||
**Decision**: `BehaviorSubject<Toast | null>` singleton service, one active toast at a time — new toasts replace the current one.
|
||||
|
||||
**Rationale**: The simplest approach that satisfies FR-007 (reusable from anywhere) and FR-008 (multiple toasts don't overlap illegibly). A queue adds complexity with no meaningful UX benefit for this app's usage pattern (copy URL, upload confirm, etc. — actions that don't overlap in practice). Replacing the current toast on rapid successive calls is acceptable and visually cleaner than a stack. The `BehaviorSubject` integrates naturally with Angular's `async` pipe and OnPush change detection.
|
||||
|
||||
**Alternatives considered**:
|
||||
- `Subject` (not `BehaviorSubject`): Late subscribers miss toasts that already fired. Rejected — component may subscribe after service emits if change detection is deferred.
|
||||
- Toast queue (array): Adds observable complexity and UI layout decisions. Rejected — over-engineered for this use case.
|
||||
- Angular CDK Overlay: Official but heavy. Pulls in CDK dependency for a feature that needs ~30 lines of code. Rejected per §2.6 (no speculative abstraction) and §6 (no new dependencies).
|
||||
|
||||
---
|
||||
|
||||
## Decision 2: Clipboard API Usage
|
||||
|
||||
**Decision**: `navigator.clipboard.writeText(url)` — no polyfill, no fallback to `document.execCommand`.
|
||||
|
||||
**Rationale**: `execCommand('copy')` is deprecated and removed in some browsers. The Clipboard API is supported in all modern browsers (Chrome 66+, Firefox 63+, Safari 13.1+). The app already requires HTTPS in production (Let's Encrypt via cert-manager), which satisfies the Clipboard API's secure context requirement. On failure (permission denied, API unavailable), catch the rejected Promise and show an error toast.
|
||||
|
||||
**Alternatives considered**:
|
||||
- `execCommand('copy')` fallback: Deprecated, inconsistent, adds code complexity. The failure path (error toast) covers the rare unavailability case more cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Decision 3: What URL to Copy
|
||||
|
||||
**Decision**: Copy `image.file_url` as-is (the direct image file URL).
|
||||
|
||||
**Rationale**: `file_url` is the CDN URL in production (e.g. `https://cdn.reactbin.juggalol.com/…`) — already absolute. In development it is relative (`/api/v1/images/{id}/file`); for dev use, prepend `window.location.origin`. The direct file URL is the right thing to share for a reaction image library: it embeds inline when pasted into Discord/Slack without requiring a click-through.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Detail page URL (`/images/{id}`): The user can already copy this from the browser address bar. The file URL is the value-add.
|
||||
- Always prepend `window.location.origin`: Works for both environments, adds a guard. Included as a defensive measure for the dev case.
|
||||
|
||||
---
|
||||
|
||||
## Decision 4: Toast Positioning
|
||||
|
||||
**Decision**: Fixed position, bottom-center of the viewport.
|
||||
|
||||
**Rationale**: Bottom-center is less intrusive than top-right for a brief confirmation toast. It doesn't overlap the image or the copy button. `pointer-events: none` ensures it never blocks interaction.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Top-right: Common convention (Material, Bootstrap) but overlaps the header/nav area in this layout.
|
||||
- Top-center: Similar issue.
|
||||
|
||||
---
|
||||
|
||||
## Decision 5: OnPush compatibility
|
||||
|
||||
**Decision**: `ToastComponent` uses `ChangeDetectionStrategy.OnPush` with the `async` pipe consuming `toastService.current$`. Angular's `async` pipe calls `markForCheck()` automatically when the observable emits, making it fully compatible with OnPush.
|
||||
|
||||
**Rationale**: Consistent with all other components in the project. No manual `markForCheck()` calls needed in `ToastComponent`.
|
||||
Reference in New Issue
Block a user