diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-12-24 15:15:46 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-24 13:15:46 +0000 |
| commit | ef27670f5c027be87d279b9b32553e980e55d888 (patch) | |
| tree | aa30e49787c17499abbe7a12b7a3353d13dbec7d /apps/web/components/dashboard/lists | |
| parent | f7d3462790652c6e5ecd90ae0d699e05b0320a97 (diff) | |
| download | karakeep-ef27670f5c027be87d279b9b32553e980e55d888.tar.zst | |
feat: show bookmark owner icon in shared lists (#2277)
* feat: Add owner icon to bookmarks in shared lists
Display a small icon showing the bookmark owner's name and email on hover when viewing bookmarks from other users in shared lists. The icon appears in the top-right corner of bookmark cards across all layout types (grid, list, compact).
Changes:
- Add user field to ZBookmark type to include owner name and email
- Update bookmark queries to fetch user information via join
- Create BookmarkOwnerIcon component with tooltip showing owner details
- Integrate owner indicator into BookmarkLayoutAdaptingCard for all layouts
- Only show icon for bookmarks not owned by current user
* use icons in more places
* remove tooltip providers
* fix non list context
---------
Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to 'apps/web/components/dashboard/lists')
| -rw-r--r-- | apps/web/components/dashboard/lists/ListHeader.tsx | 64 | ||||
| -rw-r--r-- | apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx | 67 |
2 files changed, 89 insertions, 42 deletions
diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx index 8e014e2a..ecbb6431 100644 --- a/apps/web/components/dashboard/lists/ListHeader.tsx +++ b/apps/web/components/dashboard/lists/ListHeader.tsx @@ -6,11 +6,11 @@ import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, - TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; -import { MoreHorizontal, SearchIcon, Users } from "lucide-react"; +import { MoreHorizontal, SearchIcon } from "lucide-react"; import { api } from "@karakeep/shared-react/trpc"; import { parseSearchQuery } from "@karakeep/shared/searchQueryParser"; @@ -35,6 +35,16 @@ export default function ListHeader({ }, ); + const { data: collaboratorsData } = api.lists.getCollaborators.useQuery( + { + listId: initialData.id, + }, + { + refetchOnWindowFocus: false, + enabled: list.hasCollaborators, + }, + ); + const parsedQuery = useMemo(() => { if (!list.query) { return null; @@ -55,22 +65,44 @@ export default function ListHeader({ <span className="text-2xl"> {list.icon} {list.name} </span> - {list.hasCollaborators && ( - <TooltipProvider> - <Tooltip> - <TooltipTrigger asChild> - <Users className="size-5 text-primary" /> - </TooltipTrigger> - <TooltipContent> - <p>{t("lists.shared")}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> + {list.hasCollaborators && collaboratorsData && ( + <div className="group flex"> + {collaboratorsData.owner && ( + <Tooltip> + <TooltipTrigger> + <div className="-mr-2 transition-all duration-300 ease-out group-hover:mr-1"> + <UserAvatar + name={collaboratorsData.owner.name} + image={collaboratorsData.owner.image} + className="size-5 shrink-0 rounded-full ring-2 ring-background" + /> + </div> + </TooltipTrigger> + <TooltipContent> + <p>{collaboratorsData.owner.name}</p> + </TooltipContent> + </Tooltip> + )} + {collaboratorsData.collaborators.map((collab) => ( + <Tooltip key={collab.userId}> + <TooltipTrigger> + <div className="-mr-2 transition-all duration-300 ease-out group-hover:mr-1"> + <UserAvatar + name={collab.user.name} + image={collab.user.image} + className="size-5 shrink-0 rounded-full ring-2 ring-background" + /> + </div> + </TooltipTrigger> + <TooltipContent> + <p>{collab.user.name}</p> + </TooltipContent> + </Tooltip> + ))} + </div> )} {list.description && ( - <span className="text-lg text-gray-400"> - {`(${list.description})`} - </span> + <span className="text-lg text-gray-400">{`(${list.description})`}</span> )} </div> <div className="flex items-center"> diff --git a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx index 0a55c5fe..80dbcf65 100644 --- a/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx +++ b/apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx @@ -23,6 +23,7 @@ import { SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; +import { UserAvatar } from "@/components/ui/user-avatar"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { Loader2, Trash2, UserPlus, Users } from "lucide-react"; @@ -256,15 +257,22 @@ export function ManageCollaboratorsModal({ key={`owner-${collaboratorsData.owner.id}`} className="flex items-center justify-between rounded-lg border p-3" > - <div className="flex-1"> - <div className="font-medium"> - {collaboratorsData.owner.name} - </div> - {collaboratorsData.owner.email && ( - <div className="text-sm text-muted-foreground"> - {collaboratorsData.owner.email} + <div className="flex flex-1 items-center gap-3"> + <UserAvatar + name={collaboratorsData.owner.name} + image={collaboratorsData.owner.image} + className="size-10 ring-1 ring-border" + /> + <div className="flex-1"> + <div className="font-medium"> + {collaboratorsData.owner.name} </div> - )} + {collaboratorsData.owner.email && ( + <div className="text-sm text-muted-foreground"> + {collaboratorsData.owner.email} + </div> + )} + </div> </div> <div className="text-sm capitalize text-muted-foreground"> {t("lists.collaborators.owner")} @@ -278,27 +286,34 @@ export function ManageCollaboratorsModal({ key={collaborator.id} className="flex items-center justify-between rounded-lg border p-3" > - <div className="flex-1"> - <div className="flex items-center gap-2"> - <div className="font-medium"> - {collaborator.user.name} + <div className="flex flex-1 items-center gap-3"> + <UserAvatar + name={collaborator.user.name} + image={collaborator.user.image} + className="size-10 ring-1 ring-border" + /> + <div className="flex-1"> + <div className="flex items-center gap-2"> + <div className="font-medium"> + {collaborator.user.name} + </div> + {collaborator.status === "pending" && ( + <Badge variant="outline" className="text-xs"> + {t("lists.collaborators.pending")} + </Badge> + )} + {collaborator.status === "declined" && ( + <Badge variant="destructive" className="text-xs"> + {t("lists.collaborators.declined")} + </Badge> + )} </div> - {collaborator.status === "pending" && ( - <Badge variant="outline" className="text-xs"> - {t("lists.collaborators.pending")} - </Badge> - )} - {collaborator.status === "declined" && ( - <Badge variant="destructive" className="text-xs"> - {t("lists.collaborators.declined")} - </Badge> + {collaborator.user.email && ( + <div className="text-sm text-muted-foreground"> + {collaborator.user.email} + </div> )} </div> - {collaborator.user.email && ( - <div className="text-sm text-muted-foreground"> - {collaborator.user.email} - </div> - )} </div> {readOnly ? ( <div className="text-sm capitalize text-muted-foreground"> |
