2026-01-12 15:47:56 +02:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
import { Settings, Trash2, Users } from "lucide-react";
|
|
|
|
|
import { useTranslations } from "next-intl";
|
2026-02-09 16:49:11 -08:00
|
|
|
import { useCallback, useRef, useState } from "react";
|
2026-01-13 01:45:58 -08:00
|
|
|
import {
|
|
|
|
|
ContextMenu,
|
|
|
|
|
ContextMenuContent,
|
|
|
|
|
ContextMenuItem,
|
|
|
|
|
ContextMenuSeparator,
|
|
|
|
|
ContextMenuTrigger,
|
|
|
|
|
} from "@/components/ui/context-menu";
|
2026-02-06 19:18:40 +05:30
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2026-01-12 15:47:56 +02:00
|
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
interface SearchSpaceAvatarProps {
|
|
|
|
|
name: string;
|
|
|
|
|
isActive?: boolean;
|
2026-01-13 01:45:58 -08:00
|
|
|
isShared?: boolean;
|
|
|
|
|
isOwner?: boolean;
|
2026-01-12 15:47:56 +02:00
|
|
|
onClick?: () => void;
|
2026-01-13 01:45:58 -08:00
|
|
|
onDelete?: () => void;
|
|
|
|
|
onSettings?: () => void;
|
2026-01-12 15:47:56 +02:00
|
|
|
size?: "sm" | "md";
|
2026-02-06 18:35:29 +05:30
|
|
|
disableTooltip?: boolean;
|
2026-01-12 15:47:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates a consistent color based on search space name
|
|
|
|
|
*/
|
|
|
|
|
function stringToColor(str: string): string {
|
|
|
|
|
let hash = 0;
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
|
|
|
}
|
|
|
|
|
const colors = [
|
|
|
|
|
"#6366f1", // indigo
|
|
|
|
|
"#22c55e", // green
|
|
|
|
|
"#f59e0b", // amber
|
|
|
|
|
"#ef4444", // red
|
|
|
|
|
"#8b5cf6", // violet
|
|
|
|
|
"#06b6d4", // cyan
|
|
|
|
|
"#ec4899", // pink
|
|
|
|
|
"#14b8a6", // teal
|
|
|
|
|
];
|
|
|
|
|
return colors[Math.abs(hash) % colors.length];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets initials from search space name (max 2 chars)
|
|
|
|
|
*/
|
|
|
|
|
function getInitials(name: string): string {
|
|
|
|
|
const words = name.trim().split(/\s+/);
|
|
|
|
|
if (words.length >= 2) {
|
|
|
|
|
return (words[0][0] + words[1][0]).toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
return name.slice(0, 2).toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 14:17:15 -08:00
|
|
|
export function SearchSpaceAvatar({
|
|
|
|
|
name,
|
|
|
|
|
isActive,
|
2026-01-13 01:45:58 -08:00
|
|
|
isShared,
|
|
|
|
|
isOwner = true,
|
2026-01-12 14:17:15 -08:00
|
|
|
onClick,
|
2026-01-13 01:45:58 -08:00
|
|
|
onDelete,
|
|
|
|
|
onSettings,
|
2026-01-12 14:17:15 -08:00
|
|
|
size = "md",
|
2026-02-06 18:35:29 +05:30
|
|
|
disableTooltip = false,
|
2026-01-12 14:17:15 -08:00
|
|
|
}: SearchSpaceAvatarProps) {
|
2026-01-13 01:45:58 -08:00
|
|
|
const t = useTranslations("searchSpace");
|
|
|
|
|
const tCommon = useTranslations("common");
|
2026-01-12 15:47:56 +02:00
|
|
|
const bgColor = stringToColor(name);
|
|
|
|
|
const initials = getInitials(name);
|
|
|
|
|
const sizeClasses = size === "sm" ? "h-8 w-8 text-xs" : "h-10 w-10 text-sm";
|
|
|
|
|
|
2026-02-06 19:18:40 +05:30
|
|
|
// Long-press state for mobile
|
|
|
|
|
const [longPressMenuOpen, setLongPressMenuOpen] = useState(false);
|
|
|
|
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
const touchMoved = useRef(false);
|
|
|
|
|
|
|
|
|
|
const handleTouchStart = useCallback(() => {
|
|
|
|
|
touchMoved.current = false;
|
|
|
|
|
longPressTimer.current = setTimeout(() => {
|
|
|
|
|
if (!touchMoved.current) {
|
|
|
|
|
setLongPressMenuOpen(true);
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleTouchMove = useCallback(() => {
|
|
|
|
|
touchMoved.current = true;
|
|
|
|
|
if (longPressTimer.current) {
|
|
|
|
|
clearTimeout(longPressTimer.current);
|
|
|
|
|
longPressTimer.current = null;
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleTouchEnd = useCallback(() => {
|
|
|
|
|
if (longPressTimer.current) {
|
|
|
|
|
clearTimeout(longPressTimer.current);
|
|
|
|
|
longPressTimer.current = null;
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
const tooltipContent = (
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<span>{name}</span>
|
|
|
|
|
{isShared && (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{isOwner ? tCommon("owner") : tCommon("shared")}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const avatarButton = (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
className={cn(
|
2026-02-12 18:49:52 +05:30
|
|
|
"relative flex items-center justify-center rounded-lg font-semibold text-white transition-all select-none",
|
2026-01-13 01:45:58 -08:00
|
|
|
"hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
|
|
|
sizeClasses,
|
|
|
|
|
isActive && "ring-2 ring-primary ring-offset-1 ring-offset-background"
|
|
|
|
|
)}
|
|
|
|
|
style={{ backgroundColor: bgColor }}
|
|
|
|
|
>
|
|
|
|
|
{initials}
|
|
|
|
|
{/* Shared indicator badge */}
|
|
|
|
|
{isShared && (
|
|
|
|
|
<span
|
2026-01-12 15:47:56 +02:00
|
|
|
className={cn(
|
2026-01-13 01:45:58 -08:00
|
|
|
"absolute -top-1 -right-1 flex items-center justify-center rounded-full bg-blue-500 text-white shadow-sm",
|
|
|
|
|
size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4"
|
2026-01-12 15:47:56 +02:00
|
|
|
)}
|
2026-01-13 01:45:58 -08:00
|
|
|
title={tCommon("shared")}
|
2026-01-12 15:47:56 +02:00
|
|
|
>
|
2026-01-13 01:45:58 -08:00
|
|
|
<Users className={cn(size === "sm" ? "h-2 w-2" : "h-2.5 w-2.5")} />
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-06 19:18:40 +05:30
|
|
|
const menuItems = (
|
|
|
|
|
<>
|
|
|
|
|
{onSettings && (
|
|
|
|
|
<DropdownMenuItem onClick={onSettings}>
|
|
|
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
|
|
|
{tCommon("settings")}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
{onSettings && onDelete && <DropdownMenuSeparator />}
|
|
|
|
|
{onDelete && isOwner && (
|
|
|
|
|
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
{tCommon("delete")}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
{onDelete && !isOwner && (
|
|
|
|
|
<DropdownMenuItem variant="destructive" onClick={onDelete}>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
{t("leave")}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
// If delete or settings handlers are provided, wrap with context menu
|
|
|
|
|
if (onDelete || onSettings) {
|
2026-02-06 19:18:40 +05:30
|
|
|
// Mobile: use long-press triggered DropdownMenu
|
|
|
|
|
if (disableTooltip) {
|
|
|
|
|
return (
|
|
|
|
|
<DropdownMenu open={longPressMenuOpen} onOpenChange={setLongPressMenuOpen}>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<div
|
|
|
|
|
className="inline-block"
|
|
|
|
|
onTouchStart={handleTouchStart}
|
|
|
|
|
onTouchMove={handleTouchMove}
|
|
|
|
|
onTouchEnd={handleTouchEnd}
|
|
|
|
|
onTouchCancel={handleTouchEnd}
|
|
|
|
|
>
|
|
|
|
|
{avatarButton}
|
|
|
|
|
</div>
|
|
|
|
|
</DropdownMenuTrigger>
|
2026-02-06 19:45:22 +05:30
|
|
|
<DropdownMenuContent className="w-48">{menuItems}</DropdownMenuContent>
|
2026-02-06 19:18:40 +05:30
|
|
|
</DropdownMenu>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Desktop: use right-click ContextMenu + Tooltip
|
2026-01-13 01:45:58 -08:00
|
|
|
return (
|
|
|
|
|
<ContextMenu>
|
2026-02-06 19:18:40 +05:30
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<ContextMenuTrigger asChild>
|
|
|
|
|
<div className="inline-block">{avatarButton}</div>
|
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="right" sideOffset={8}>
|
|
|
|
|
{tooltipContent}
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-01-13 01:45:58 -08:00
|
|
|
<ContextMenuContent className="w-48">
|
|
|
|
|
{onSettings && (
|
|
|
|
|
<ContextMenuItem onClick={onSettings}>
|
|
|
|
|
<Settings className="mr-2 h-4 w-4" />
|
|
|
|
|
{tCommon("settings")}
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
{onSettings && onDelete && <ContextMenuSeparator />}
|
|
|
|
|
{onDelete && isOwner && (
|
|
|
|
|
<ContextMenuItem variant="destructive" onClick={onDelete}>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
{tCommon("delete")}
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
{onDelete && !isOwner && (
|
|
|
|
|
<ContextMenuItem variant="destructive" onClick={onDelete}>
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
{t("leave")}
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</ContextMenuContent>
|
|
|
|
|
</ContextMenu>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No context menu needed
|
2026-02-06 18:35:29 +05:30
|
|
|
if (disableTooltip) {
|
|
|
|
|
return avatarButton;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 01:45:58 -08:00
|
|
|
return (
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>{avatarButton}</TooltipTrigger>
|
2026-01-12 15:47:56 +02:00
|
|
|
<TooltipContent side="right" sideOffset={8}>
|
2026-01-13 01:45:58 -08:00
|
|
|
{tooltipContent}
|
2026-01-12 15:47:56 +02:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
);
|
|
|
|
|
}
|