@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 CHANGED
@@ -1,3 +1,386 @@
1
- # Temporary Holding Version
1
+ # @inovusmedical/avatar
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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