aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard/lists
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-12-24 15:15:46 +0200
committerGitHub <noreply@github.com>2025-12-24 13:15:46 +0000
commitef27670f5c027be87d279b9b32553e980e55d888 (patch)
treeaa30e49787c17499abbe7a12b7a3353d13dbec7d /apps/web/components/dashboard/lists
parentf7d3462790652c6e5ecd90ae0d699e05b0320a97 (diff)
downloadkarakeep-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.tsx64
-rw-r--r--apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx67
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">