feat: add user centric features

This commit is contained in:
gosangam
2026-05-30 23:44:08 +05:30
parent 7bd2952caa
commit dcdd2383c6
63 changed files with 16783 additions and 1735 deletions
+63 -79
View File
@@ -1,9 +1,18 @@
"use client";
import { useEffect, useRef } from "react";
/**
* MorePopover — generic secondary-action menu for the selection toolbar.
*
* The parent (`TextSelectionToolbar`) owns all side effects and builds
* the appropriate `items` array based on auth state. Items can be marked
* `locked` to render a small Lock badge — used for the public flow
* where every More action sits behind the auth wall.
*/
import { type ReactNode, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useCopyFeedback, type Position } from "./shared";
import { Bookmark, FolderPlus, AtSign } from "lucide-react";
import { Lock } from "lucide-react";
import { type Position } from "./shared";
import {
Drawer,
DrawerClose,
@@ -12,35 +21,37 @@ import {
DrawerTitle,
} from "@/components/ui/drawer";
export type MoreItem = {
key: string;
label: string;
description: string;
icon: ReactNode;
onClick: () => void;
/** Show a lock badge — indicates the action is behind the auth wall. */
locked?: boolean;
};
export function MorePopover({
open,
position,
onClose,
onBookmark,
onSaveToCollection,
onMention,
items,
isMobile,
selectedText,
}: {
open: boolean;
position: Position;
onClose: () => void;
onBookmark: () => void;
onSaveToCollection: () => void;
onMention: () => void;
items: MoreItem[];
isMobile: boolean;
selectedText: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const { copied, copy } = useCopyFeedback();
useEffect(() => {
if (!open || isMobile) return;
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
onClose();
}
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
}
// Delay so the click that opened us doesn't immediately fire it.
const timer = setTimeout(() => {
document.addEventListener("mousedown", handleClick);
}, 10);
@@ -52,30 +63,6 @@ export function MorePopover({
if (!open) return null;
const actions = [
{
key: "bookmark",
label: "Add Bookmark",
description: "Save your place",
icon: <Bookmark className="size-5" />,
onClick: onBookmark,
},
{
key: "collection",
label: "Save to Collection",
description: "Organize into a collection",
icon: <FolderPlus className="size-5" />,
onClick: onSaveToCollection,
},
{
key: "mention",
label: "Mention",
description: "Tag someone with this quote",
icon: <AtSign className="size-5" />,
onClick: onMention,
},
];
// Mobile: bottom drawer
if (isMobile) {
return (
@@ -85,27 +72,29 @@ export function MorePopover({
<DrawerTitle className="text-white">More Options</DrawerTitle>
</DrawerHeader>
<div className="p-4 space-y-1">
{actions.map((action) => (
{items.map((item) => (
<button
key={action.key}
key={item.key}
onClick={(e) => {
e.stopPropagation();
action.onClick();
if (action.key !== "copy-link") {
onClose();
}
item.onClick();
}}
className="flex items-center gap-3 w-full px-3 py-3 rounded-lg text-left hover:bg-white/10 transition-colors"
>
<div className="flex items-center justify-center size-10 rounded-lg bg-white/10 text-white/60">
{action.icon}
{item.icon}
</div>
<div>
<p className="text-sm font-medium text-white/90">
{action.label}
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-white/90 truncate">
{item.label}
</p>
<p className="text-xs text-white/50 truncate">
{item.description}
</p>
<p className="text-xs text-white/50">{action.description}</p>
</div>
{item.locked ? (
<Lock className="size-3.5 text-white/40 shrink-0" />
) : null}
</button>
))}
</div>
@@ -121,54 +110,49 @@ export function MorePopover({
);
}
// Desktop: floating popover
const popoverW = 220;
const popoverH = actions.length * 44 + 8;
// Desktop: floating popover anchored to `position`, flipping when it
// would overflow the viewport.
const popoverW = 260;
const popoverH = items.length * 56 + 8;
const pad = 8;
let popLeft = position.x;
let popTop = position.y + 8;
if (popLeft + popoverW > window.innerWidth - pad) {
popLeft = window.innerWidth - popoverW - pad;
}
if (popLeft < pad) {
popLeft = pad;
}
if (popLeft < pad) popLeft = pad;
if (popTop + popoverH > window.innerHeight - pad) {
popTop = position.y - popoverH - 8;
}
if (popTop < pad) {
popTop = pad;
}
const style: React.CSSProperties = {
position: "fixed",
zIndex: 90,
left: popLeft,
top: popTop,
};
if (popTop < pad) popTop = pad;
return createPortal(
<div
ref={ref}
style={style}
className="w-55 rounded-lg border border-white/10 bg-black shadow-2xl py-1.5 animate-in fade-in-0 zoom-in-95 dark:bg-[#1a1a1a] dark:border-white/15"
style={{ position: "fixed", zIndex: 90, left: popLeft, top: popTop }}
className="w-64 rounded-lg border border-white/10 bg-black shadow-2xl py-1.5 animate-in fade-in-0 zoom-in-95 dark:bg-[#1a1a1a] dark:border-white/15"
>
{actions.map((action) => (
{items.map((item) => (
<button
key={action.key}
key={item.key}
onClick={(e) => {
e.stopPropagation();
action.onClick();
if (action.key !== "copy-link") {
onClose();
}
item.onClick();
}}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-white/90 hover:bg-white/10 transition-colors text-left"
className="flex items-start gap-3 w-full px-3 py-2.5 text-left hover:bg-white/10 transition-colors"
>
<span className="text-white/50">{action.icon}</span>
{action.label}
<span className="text-white/60 mt-0.5">{item.icon}</span>
<span className="min-w-0 flex-1">
<span className="block text-sm text-white/90 truncate">
{item.label}
</span>
<span className="block text-xs text-white/50 truncate">
{item.description}
</span>
</span>
{item.locked ? (
<Lock className="size-3.5 text-white/40 shrink-0 mt-1" />
) : null}
</button>
))}
</div>,