-
Notifications
You must be signed in to change notification settings - Fork 0
add presence #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
add presence #322
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
packages/web/src/components/project/reconcile-tab/PresenceAvatars.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| /** | ||
| * PresenceAvatars - Displays stacked avatars of users viewing the reconciliation | ||
| * | ||
| * Features: | ||
| * - Stacked avatar display with overlap | ||
| * - Tooltip showing user name and current question | ||
| * - Click to jump to user's current question | ||
| * - Overflow indicator for many users | ||
| */ | ||
|
|
||
| import { For, Show } from 'solid-js'; | ||
| import { Avatar, AvatarImage, AvatarFallback, getInitials } from '@/components/ui/avatar'; | ||
| import { | ||
| Tooltip, | ||
| TooltipTrigger, | ||
| TooltipPositioner, | ||
| TooltipContent, | ||
| } from '@/components/ui/tooltip'; | ||
| import { API_BASE } from '@config/api.js'; | ||
|
|
||
| /** | ||
| * @param {Object} props | ||
| * @param {Array} props.users - Array of presence users with { userId, name, image, currentPage, color } | ||
| * @param {Function} props.onUserClick - Callback when user avatar is clicked (receives userId, currentPage) | ||
| * @param {number} [props.maxVisible=4] - Maximum number of avatars to show before overflow | ||
| * @param {Function} [props.getPageLabel] - Function to convert page index to display label (default: "Question N") | ||
| */ | ||
| export default function PresenceAvatars(props) { | ||
| const maxVisible = () => props.maxVisible ?? 4; | ||
| const visibleUsers = () => props.users.slice(0, maxVisible()); | ||
| const overflowCount = () => Math.max(0, props.users.length - maxVisible()); | ||
|
|
||
| const getPageLabel = pageIndex => { | ||
| if (props.getPageLabel) { | ||
| return props.getPageLabel(pageIndex); | ||
| } | ||
| return `Question ${pageIndex + 1}`; | ||
| }; | ||
|
|
||
| // Build avatar URL from user data | ||
| const getAvatarUrl = user => { | ||
| if (user.image) { | ||
| // If it's a full URL, use it directly | ||
| if (user.image.startsWith('http')) { | ||
| return user.image; | ||
| } | ||
| // Otherwise, it's a path to our API | ||
| return `${API_BASE}/api/users/avatar/${user.userId}`; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| return ( | ||
| <div class='flex items-center gap-2'> | ||
| <div class='flex -space-x-2'> | ||
| <For each={visibleUsers()}> | ||
| {user => ( | ||
| <Tooltip openDelay={200} positioning={{ placement: 'bottom' }}> | ||
| <TooltipTrigger> | ||
| <button | ||
| onClick={() => props.onUserClick?.(user.userId, user.currentPage)} | ||
| class='focus:ring-primary relative rounded-full transition-transform hover:z-10 hover:scale-110 focus:z-10 focus:ring-2 focus:ring-offset-2 focus:outline-none' | ||
| style={{ | ||
| 'box-shadow': `0 0 0 2px ${user.color.hex}`, | ||
| }} | ||
| > | ||
| <Avatar class='h-7 w-7 border-2 border-white text-xs'> | ||
| <AvatarImage src={getAvatarUrl(user)} alt={user.name} /> | ||
| <AvatarFallback | ||
| class={`${user.color.bg} text-white`} | ||
| style={{ 'background-color': user.color.hex }} | ||
| > | ||
| {getInitials(user.name)} | ||
| </AvatarFallback> | ||
| </Avatar> | ||
| </button> | ||
| </TooltipTrigger> | ||
| <TooltipPositioner> | ||
| <TooltipContent class='flex flex-col gap-0.5'> | ||
| <span class='font-medium'>{user.name}</span> | ||
| <span class='text-muted-foreground text-xs'> | ||
| Viewing {getPageLabel(user.currentPage)} | ||
| </span> | ||
| </TooltipContent> | ||
| </TooltipPositioner> | ||
| </Tooltip> | ||
| )} | ||
| </For> | ||
|
|
||
| {/* Overflow indicator */} | ||
| <Show when={overflowCount() > 0}> | ||
| <Tooltip openDelay={200} positioning={{ placement: 'bottom' }}> | ||
| <TooltipTrigger> | ||
| <div class='bg-muted text-muted-foreground flex h-7 w-7 items-center justify-center rounded-full border-2 border-white text-xs font-medium'> | ||
| +{overflowCount()} | ||
| </div> | ||
| </TooltipTrigger> | ||
| <TooltipPositioner> | ||
| <TooltipContent> | ||
| {overflowCount()} more {overflowCount() === 1 ? 'person' : 'people'} viewing | ||
| </TooltipContent> | ||
| </TooltipPositioner> | ||
| </Tooltip> | ||
| </Show> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
63 changes: 63 additions & 0 deletions
63
packages/web/src/components/project/reconcile-tab/QuestionPresenceIndicator.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| /** | ||
| * QuestionPresenceIndicator - Colored rings on question pills showing who's viewing | ||
| * | ||
| * Features: | ||
| * - Renders colored ring(s) around the parent element | ||
| * - Supports multiple users with stacked rings | ||
| * - Pulsing animation for visibility | ||
| */ | ||
|
|
||
| import { For, Show } from 'solid-js'; | ||
|
|
||
| /** | ||
| * @param {Object} props | ||
| * @param {Array} props.users - Array of users viewing this question with { userId, color, name } | ||
| * @param {number} [props.maxRings=2] - Maximum number of rings to show | ||
| * @param {string} [props.size='md'] - Ring size: 'sm', 'md', 'lg' | ||
| */ | ||
| export default function QuestionPresenceIndicator(props) { | ||
| const users = () => props.users ?? []; | ||
| const maxRings = () => props.maxRings ?? 2; | ||
| const visibleUsers = () => users().slice(0, maxRings()); | ||
|
|
||
| // Ring offsets for stacking effect | ||
| const getOffset = index => { | ||
| const offsets = { | ||
| sm: 2, | ||
| md: 3, | ||
| lg: 4, | ||
| }; | ||
| const base = offsets[props.size || 'md'] || 3; | ||
| return index * base; | ||
| }; | ||
|
|
||
| // Ring width - thicker for better visibility | ||
| const getRingWidth = () => { | ||
| const widths = { | ||
| sm: 2, | ||
| md: 3, | ||
| lg: 4, | ||
| }; | ||
| return widths[props.size || 'md'] || 3; | ||
| }; | ||
|
|
||
| return ( | ||
| <Show when={users().length > 0}> | ||
| <div class='pointer-events-none absolute inset-0'> | ||
| <For each={visibleUsers()}> | ||
| {(user, index) => ( | ||
| <div | ||
| class='absolute inset-0 animate-pulse rounded-full' | ||
| style={{ | ||
| margin: `-${getOffset(index())}px`, | ||
| border: `${getRingWidth()}px solid ${user.color.hex}`, | ||
| opacity: 0.85 - index() * 0.15, | ||
| 'box-shadow': `0 0 8px ${user.color.hex}50`, | ||
| }} | ||
| /> | ||
| )} | ||
| </For> | ||
| </div> | ||
| </Show> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.