@inovusmedical/avatar 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +385 -2
- package/dist/index.cjs +2088 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +2072 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +388 -0
- package/package.json +62 -5
package/README.md
CHANGED
|
@@ -1,3 +1,386 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @inovusmedical/avatar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React components and hooks for user avatars in the Totum app ecosystem.
|
|
4
|
+
|
|
5
|
+
Uses the shared `user_data` Supabase project directly via Third-Party Auth with Cognito.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @inovusmedical/avatar
|
|
11
|
+
# or
|
|
12
|
+
pnpm add @inovusmedical/avatar
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Peer Dependencies
|
|
16
|
+
|
|
17
|
+
- `react` ^18.0.0 || ^19.0.0
|
|
18
|
+
- `react-dom` ^18.0.0 || ^19.0.0
|
|
19
|
+
- `@tanstack/react-query` ^5.0.0
|
|
20
|
+
- `@supabase/supabase-js` ^2.0.0
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Add the Provider
|
|
25
|
+
|
|
26
|
+
Wrap your app with `AvatarProvider`:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { AvatarProvider } from '@inovusmedical/avatar';
|
|
30
|
+
import '@inovusmedical/avatar/styles';
|
|
31
|
+
import { getAccessToken } from '@inovusmedical/unified-auth';
|
|
32
|
+
|
|
33
|
+
function App() {
|
|
34
|
+
return (
|
|
35
|
+
<AvatarProvider
|
|
36
|
+
supabaseAnonKey={import.meta.env.VITE_USER_DATA_ANON_KEY}
|
|
37
|
+
getAccessToken={getAccessToken}
|
|
38
|
+
>
|
|
39
|
+
<YourApp />
|
|
40
|
+
</AvatarProvider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `supabaseUrl` defaults to the `user_data` project. Only override if using a different Supabase instance.
|
|
46
|
+
|
|
47
|
+
### 2. Display Avatars
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { UserAvatar } from '@inovusmedical/avatar';
|
|
51
|
+
|
|
52
|
+
function UserCard({ userId, name }) {
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<UserAvatar userId={userId} name={name} size="lg" />
|
|
56
|
+
<span>{name}</span>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Upload Avatars
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { AvatarUploader } from '@inovusmedical/avatar';
|
|
66
|
+
|
|
67
|
+
function ProfileSettings() {
|
|
68
|
+
return (
|
|
69
|
+
<AvatarUploader
|
|
70
|
+
onUploadSuccess={(url) => console.log('Uploaded:', url)}
|
|
71
|
+
onUploadError={(error) => console.error('Error:', error)}
|
|
72
|
+
allowWebcam={true}
|
|
73
|
+
showRemoveButton={true}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Integration Guide for totum-live
|
|
80
|
+
|
|
81
|
+
### Step 1: Install the package
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm add @inovusmedical/avatar @supabase/supabase-js
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Step 2: Add environment variable
|
|
88
|
+
|
|
89
|
+
Add to your `.env`:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
VITE_USER_DATA_ANON_KEY=your-user-data-anon-key
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Get the anon key from the `user_data` Supabase project (Settings → API → anon public key).
|
|
96
|
+
|
|
97
|
+
### Step 3: Set up the provider
|
|
98
|
+
|
|
99
|
+
In your main `App.tsx` or layout component:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
103
|
+
import { AvatarProvider } from '@inovusmedical/avatar';
|
|
104
|
+
import '@inovusmedical/avatar/styles';
|
|
105
|
+
import { getAccessToken } from '@inovusmedical/unified-auth';
|
|
106
|
+
|
|
107
|
+
const queryClient = new QueryClient();
|
|
108
|
+
|
|
109
|
+
function App() {
|
|
110
|
+
return (
|
|
111
|
+
<QueryClientProvider client={queryClient}>
|
|
112
|
+
<AvatarProvider
|
|
113
|
+
supabaseAnonKey={import.meta.env.VITE_USER_DATA_ANON_KEY}
|
|
114
|
+
getAccessToken={getAccessToken}
|
|
115
|
+
>
|
|
116
|
+
{/* Your app */}
|
|
117
|
+
</AvatarProvider>
|
|
118
|
+
</QueryClientProvider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Step 4: Replace Header avatar
|
|
124
|
+
|
|
125
|
+
In your `Header.tsx`:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { UserAvatar, AvatarMenu } from '@inovusmedical/avatar';
|
|
129
|
+
import { useLoggedInUser } from '@inovusmedical/unified-auth';
|
|
130
|
+
|
|
131
|
+
function Header() {
|
|
132
|
+
const { user } = useLoggedInUser();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<header>
|
|
136
|
+
{/* Option A: Just the avatar */}
|
|
137
|
+
<UserAvatar
|
|
138
|
+
userId={user.sub}
|
|
139
|
+
name={user.name}
|
|
140
|
+
size="sm"
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
{/* Option B: Avatar with dropdown menu */}
|
|
144
|
+
<AvatarMenu
|
|
145
|
+
userId={user.sub}
|
|
146
|
+
name={user.name}
|
|
147
|
+
size="sm"
|
|
148
|
+
menuItems={
|
|
149
|
+
<>
|
|
150
|
+
<a href="/profile">My Profile</a>
|
|
151
|
+
<button onClick={logout}>Sign Out</button>
|
|
152
|
+
</>
|
|
153
|
+
}
|
|
154
|
+
/>
|
|
155
|
+
</header>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Step 5: Use batch lookups for lists
|
|
161
|
+
|
|
162
|
+
For chat panels, attendee lists, and people grids:
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { useAvatars, UserAvatar } from '@inovusmedical/avatar';
|
|
166
|
+
|
|
167
|
+
function ChatPanel({ messages }) {
|
|
168
|
+
// Extract unique user IDs
|
|
169
|
+
const userIds = [...new Set(messages.map(m => m.sender_guid))];
|
|
170
|
+
|
|
171
|
+
// Batch fetch all avatars (single Supabase query)
|
|
172
|
+
const { data: avatars } = useAvatars(userIds);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div>
|
|
176
|
+
{messages.map(msg => (
|
|
177
|
+
<div key={msg.id}>
|
|
178
|
+
<UserAvatar
|
|
179
|
+
userId={msg.sender_guid}
|
|
180
|
+
name={msg.sender_name}
|
|
181
|
+
size="sm"
|
|
182
|
+
/>
|
|
183
|
+
<span>{msg.content}</span>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Step 6: Add profile page uploader
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { UserAvatar, AvatarUploader } from '@inovusmedical/avatar';
|
|
195
|
+
import { useLoggedInUser } from '@inovusmedical/unified-auth';
|
|
196
|
+
|
|
197
|
+
function ProfilePage() {
|
|
198
|
+
const { user } = useLoggedInUser();
|
|
199
|
+
const [showUploader, setShowUploader] = useState(false);
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div>
|
|
203
|
+
<UserAvatar userId={user.sub} name={user.name} size="xl" />
|
|
204
|
+
|
|
205
|
+
<button onClick={() => setShowUploader(true)}>
|
|
206
|
+
Change Photo
|
|
207
|
+
</button>
|
|
208
|
+
|
|
209
|
+
{showUploader && (
|
|
210
|
+
<AvatarUploader
|
|
211
|
+
onUploadSuccess={() => setShowUploader(false)}
|
|
212
|
+
onRemoveSuccess={() => setShowUploader(false)}
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Components
|
|
221
|
+
|
|
222
|
+
### `<AvatarProvider>`
|
|
223
|
+
|
|
224
|
+
Context provider that configures the avatar service.
|
|
225
|
+
|
|
226
|
+
| Prop | Type | Required | Description |
|
|
227
|
+
|------|------|----------|-------------|
|
|
228
|
+
| `supabaseUrl` | `string` | No | Supabase project URL (defaults to user_data) |
|
|
229
|
+
| `supabaseAnonKey` | `string` | Yes | Supabase anon/public key |
|
|
230
|
+
| `getAccessToken` | `() => Promise<string \| null>` | Yes | Function to get Cognito token |
|
|
231
|
+
| `batchDelay` | `number` | No | Debounce delay for batch requests (default: 50ms) |
|
|
232
|
+
|
|
233
|
+
### `<UserAvatar>`
|
|
234
|
+
|
|
235
|
+
Displays a user's avatar with automatic batched lookups.
|
|
236
|
+
|
|
237
|
+
| Prop | Type | Required | Description |
|
|
238
|
+
|------|------|----------|-------------|
|
|
239
|
+
| `userId` | `string` | Yes | User's Cognito sub (user_guid) |
|
|
240
|
+
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| number` | No | Avatar size (default: `'md'`) |
|
|
241
|
+
| `name` | `string` | No | User's name for initials fallback |
|
|
242
|
+
| `fallback` | `'initials' \| 'icon' \| ReactNode` | No | Fallback when no avatar (default: `'initials'`) |
|
|
243
|
+
| `className` | `string` | No | Additional CSS class |
|
|
244
|
+
| `onClick` | `() => void` | No | Click handler (makes avatar interactive) |
|
|
245
|
+
|
|
246
|
+
### `<AvatarUploader>`
|
|
247
|
+
|
|
248
|
+
Upload interface with file picker, webcam capture, and cropping.
|
|
249
|
+
|
|
250
|
+
| Prop | Type | Required | Description |
|
|
251
|
+
|------|------|----------|-------------|
|
|
252
|
+
| `onUploadSuccess` | `(url: string) => void` | No | Called after successful upload |
|
|
253
|
+
| `onUploadError` | `(error: Error) => void` | No | Called on upload error |
|
|
254
|
+
| `onRemoveSuccess` | `() => void` | No | Called after successful removal |
|
|
255
|
+
| `onRemoveError` | `(error: Error) => void` | No | Called on removal error |
|
|
256
|
+
| `showRemoveButton` | `boolean` | No | Show remove photo button (default: `true`) |
|
|
257
|
+
| `allowWebcam` | `boolean` | No | Enable webcam capture (default: `true`) |
|
|
258
|
+
| `cropShape` | `'round' \| 'rect'` | No | Crop area shape (default: `'round'`) |
|
|
259
|
+
| `maxFileSizeMB` | `number` | No | Max file size in MB (default: `10`) |
|
|
260
|
+
| `className` | `string` | No | Additional CSS class |
|
|
261
|
+
|
|
262
|
+
### `<AvatarMenu>`
|
|
263
|
+
|
|
264
|
+
Drop-in header component with avatar and change photo dropdown.
|
|
265
|
+
|
|
266
|
+
| Prop | Type | Required | Description |
|
|
267
|
+
|------|------|----------|-------------|
|
|
268
|
+
| `userId` | `string` | Yes | User's Cognito sub |
|
|
269
|
+
| `name` | `string` | No | User's name |
|
|
270
|
+
| `size` | `AvatarSize` | No | Avatar size (default: `'md'`) |
|
|
271
|
+
| `menuItems` | `ReactNode` | No | Additional menu items |
|
|
272
|
+
| `className` | `string` | No | Additional CSS class |
|
|
273
|
+
|
|
274
|
+
## Hooks
|
|
275
|
+
|
|
276
|
+
### `useAvatar(userId)`
|
|
277
|
+
|
|
278
|
+
Fetch a single user's avatar URL.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const { data: url, isLoading, error } = useAvatar('user-id');
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### `useAvatars(userIds)`
|
|
285
|
+
|
|
286
|
+
Fetch multiple avatar URLs in one batched Supabase query.
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
const { data: avatarMap, isLoading } = useAvatars(['user1', 'user2', 'user3']);
|
|
290
|
+
// avatarMap is Map<string, string | null>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### `useCurrentUserId()`
|
|
294
|
+
|
|
295
|
+
Get the current user's ID from their Cognito token.
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
const { data: userId } = useCurrentUserId();
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### `useUploadAvatar()`
|
|
302
|
+
|
|
303
|
+
Upload a new avatar.
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const { mutateAsync: upload, isPending } = useUploadAvatar();
|
|
307
|
+
|
|
308
|
+
const blob = await cropAndResizeImage(imageSrc, cropArea);
|
|
309
|
+
const result = await upload(blob);
|
|
310
|
+
console.log('New URL:', result.url);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `useDeleteAvatar()`
|
|
314
|
+
|
|
315
|
+
Delete the current user's avatar.
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
const { mutateAsync: deleteAvatar, isPending } = useDeleteAvatar();
|
|
319
|
+
|
|
320
|
+
await deleteAvatar();
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### `useDeleteAvatarAdmin()`
|
|
324
|
+
|
|
325
|
+
Admin: delete any user's avatar (requires Admin Cognito group).
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
const { mutateAsync: adminDelete } = useDeleteAvatarAdmin();
|
|
329
|
+
|
|
330
|
+
await adminDelete('target-user-id');
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Styling
|
|
334
|
+
|
|
335
|
+
Import the default styles:
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
import '@inovusmedical/avatar/styles';
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Tailwind Integration
|
|
342
|
+
|
|
343
|
+
The components use BEM-style classes that won't conflict with Tailwind:
|
|
344
|
+
|
|
345
|
+
```tsx
|
|
346
|
+
<UserAvatar
|
|
347
|
+
userId={user.sub}
|
|
348
|
+
className="ring-2 ring-blue-500"
|
|
349
|
+
/>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Architecture
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
┌─────────────────┐ ┌─────────────────────────┐
|
|
356
|
+
│ Totum Apps │ │ user_data Supabase │
|
|
357
|
+
│ (React) │────▶│ - profile_images table │
|
|
358
|
+
│ │ │ - profile-images bucket│
|
|
359
|
+
│ @inovusmedical │ │ - Third-Party Auth │
|
|
360
|
+
│ /avatar │ │ (Cognito) │
|
|
361
|
+
└─────────────────┘ └─────────────────────────┘
|
|
362
|
+
│
|
|
363
|
+
▼
|
|
364
|
+
┌─────────────────┐
|
|
365
|
+
│ unified-auth │
|
|
366
|
+
│ (Cognito JWT) │
|
|
367
|
+
└─────────────────┘
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The package connects directly to the `user_data` Supabase project using the Cognito JWT from unified-auth. No separate backend service is needed.
|
|
371
|
+
|
|
372
|
+
## Backwards Compatibility
|
|
373
|
+
|
|
374
|
+
This package is compatible with existing widgets-dashboard avatars:
|
|
375
|
+
- Reads from the same `profile_images` table
|
|
376
|
+
- Uses the same `profile-images` storage bucket
|
|
377
|
+
- Existing image paths continue to work
|
|
378
|
+
- New uploads use versioned paths to fix stale-cache issues
|
|
379
|
+
|
|
380
|
+
## Stale Cache Fix
|
|
381
|
+
|
|
382
|
+
New uploads use versioned file paths (`{user_guid}/{timestamp}-{random}.webp`) instead of overwriting the same file. This ensures browsers and CDNs always show the latest image.
|
|
383
|
+
|
|
384
|
+
## Version
|
|
385
|
+
|
|
386
|
+
0.1.0
|