docs : add user avatar feature design spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
112
docs/superpowers/specs/2026-03-15-user-avatar-design.md
Normal file
112
docs/superpowers/specs/2026-03-15-user-avatar-design.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# User Avatar — Design Spec
|
||||
|
||||
## Goal
|
||||
|
||||
Allow users to upload a profile avatar image (with client-side circular crop) that replaces initials everywhere in the app.
|
||||
|
||||
## Backend
|
||||
|
||||
### Entity Changes
|
||||
|
||||
**User** — add nullable field:
|
||||
- `avatarFileName: ?string` (length 255) — UUID-based filename stored on disk
|
||||
|
||||
### Storage
|
||||
|
||||
- Directory: `var/uploads/avatars/`
|
||||
- Parameter in `services.yaml`: `avatar_upload_dir`
|
||||
|
||||
### Endpoints
|
||||
|
||||
All under `/api/users/{id}/avatar`:
|
||||
|
||||
| Method | Description | Auth |
|
||||
|--------|-------------|------|
|
||||
| `POST` | Upload avatar (multipart file) | Owner or ROLE_ADMIN |
|
||||
| `GET` | Serve avatar image (inline) | ROLE_USER or ROLE_CLIENT |
|
||||
| `DELETE` | Remove avatar | Owner or ROLE_ADMIN |
|
||||
|
||||
**POST** accepts a single `file` field. Validates: image MIME (jpeg, png, webp, gif), max 5 MB. Stores with UUID filename, updates `avatarFileName`. Deletes previous file if exists.
|
||||
|
||||
**GET** returns the image with proper `Content-Type`. Returns 404 if no avatar.
|
||||
|
||||
**DELETE** removes file from disk, sets `avatarFileName` to null.
|
||||
|
||||
These are custom Symfony controllers (not API Platform resources) under `/api/` with `priority: 1`.
|
||||
|
||||
### Serialization
|
||||
|
||||
Add a virtual `avatarUrl` field to User serialization (group `user:read`):
|
||||
- If `avatarFileName` is set: `/api/users/{id}/avatar`
|
||||
- If null: `null`
|
||||
|
||||
This way the frontend knows if an avatar exists from any user payload.
|
||||
|
||||
### Migration
|
||||
|
||||
- Add `avatar_file_name` column (VARCHAR 255, nullable) to `user` table.
|
||||
|
||||
## Frontend
|
||||
|
||||
### New Components
|
||||
|
||||
**`UserAvatar.vue`** (`frontend/components/user/UserAvatar.vue`):
|
||||
- Props: `user: { id: number, username: string, avatarUrl?: string | null }`, `size: 'xs' | 'sm' | 'md' | 'lg'`
|
||||
- Sizes: xs=20px, sm=24px, md=32px, lg=48px
|
||||
- If `avatarUrl`: `<img>` rounded-full, object-cover
|
||||
- Else: initials badge (current bg-primary-500 style), 2 first chars of username uppercased
|
||||
- Handles `@error` on img to fallback to initials (broken image)
|
||||
|
||||
**`AvatarCropper.vue`** (`frontend/components/user/AvatarCropper.vue`):
|
||||
- Uses `vue-advanced-cropper` with `CircleStencil`
|
||||
- Props: `imageFile: File`
|
||||
- Emits: `crop(blob: Blob)`, `cancel`
|
||||
- Fixed output size: 256x256px
|
||||
- Modal overlay with crop area + confirm/cancel buttons
|
||||
|
||||
### New Page
|
||||
|
||||
**`/profile`** (`frontend/pages/profile.vue`):
|
||||
- Shows current avatar (large) with "Change" button
|
||||
- File input triggers AvatarCropper modal
|
||||
- On confirm: POST blob to `/api/users/{id}/avatar`
|
||||
- On success: refresh auth store user data
|
||||
- "Remove avatar" button if avatar exists
|
||||
- Accessible from "Mon profil" button in AppTopNav dropdown
|
||||
|
||||
### New Service
|
||||
|
||||
**`frontend/services/avatar.ts`**:
|
||||
- `upload(userId: number, file: Blob): Promise<void>` — POST multipart
|
||||
- `remove(userId: number): Promise<void>` — DELETE
|
||||
- `getUrl(userId: number): string` — returns URL path
|
||||
|
||||
### DTO Update
|
||||
|
||||
**`UserData`** — add: `avatarUrl?: string | null`
|
||||
|
||||
### Replacement Points
|
||||
|
||||
Replace initials/icon with `<UserAvatar>` in:
|
||||
|
||||
| File | Current display | Size |
|
||||
|------|----------------|------|
|
||||
| `TaskCard.vue:48-53` | Initials badge (h-5 w-5) | xs |
|
||||
| `archives.vue:50-55` | Initials badge (h-5 w-5) | xs |
|
||||
| `AppTopNav.vue:13` | `mdi:account-circle-outline` icon | md |
|
||||
| `AdminClientTicketTab.vue` | Username text for submitter | sm |
|
||||
| `ClientTicketDetailModal.vue` | submittedBy display | sm |
|
||||
|
||||
### Auth Store
|
||||
|
||||
After avatar upload/delete, re-fetch current user data so `avatarUrl` updates everywhere reactively.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `vue-advanced-cropper` — npm install in frontend/
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Server-side image processing/resize
|
||||
- Multiple image formats conversion
|
||||
- Avatar for clients (entities), only users
|
||||
Reference in New Issue
Block a user