feat: add three-dot menu to highlight cards

- Replace external link button with three-dot menu in highlight cards
- Move 'Open source/Nostr' and 'Delete' actions into dropdown menu
- Add click-outside functionality to close menu
- Style menu for both dark and light themes
This commit is contained in:
Gigi
2025-10-12 23:59:28 +02:00
parent 14cf3189b8
commit e6a7bb4c98
2 changed files with 166 additions and 32 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash } from '@fortawesome/free-solid-svg-icons'
import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash, faEllipsisV } from '@fortawesome/free-solid-svg-icons'
import { Highlight } from '../types/highlights'
import { useEventModel } from 'applesauce-react/hooks'
import { Models, IEventStore } from 'applesauce-core'
@@ -40,11 +40,13 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
onHighlightDelete
}) => {
const itemRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id))
const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing)
const [isRebroadcasting, setIsRebroadcasting] = useState(false)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const [showMenu, setShowMenu] = useState(false)
const activeAccount = Hooks.useActiveAccount()
@@ -97,6 +99,22 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
}
}, [isSelected])
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setShowMenu(false)
}
}
if (showMenu) {
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}
}, [showMenu])
const handleItemClick = () => {
if (onHighlightClick) {
onHighlightClick(highlight.id)
@@ -295,6 +313,29 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
setShowDeleteConfirm(false)
}
const handleMenuToggle = (e: React.MouseEvent) => {
e.stopPropagation()
setShowMenu(!showMenu)
}
const handleOpenExternal = (e: React.MouseEvent) => {
e.stopPropagation()
if (sourceLink) {
if (highlight.urlReference && onSelectUrl) {
onSelectUrl(highlight.urlReference)
} else {
window.open(sourceLink, '_blank', 'noopener,noreferrer')
}
}
setShowMenu(false)
}
const handleMenuDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
setShowMenu(false)
setShowDeleteConfirm(true)
}
return (
<>
<div
@@ -316,15 +357,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
<FontAwesomeIcon icon={relayIndicator.icon} spin={relayIndicator.spin} />
</div>
)}
{canDelete && (
<div
className="highlight-delete-btn"
title="Delete highlight"
onClick={handleDeleteClick}
>
<FontAwesomeIcon icon={isDeleting ? faSpinner : faTrash} spin={isDeleting} />
</div>
)}
</div>
<div className="highlight-content">
@@ -348,18 +380,39 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
{formatDateCompact(highlight.created_at)}
</span>
{sourceLink && (
<a
href={sourceLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => highlight.urlReference && onSelectUrl ? handleLinkClick(highlight.urlReference, e) : undefined}
className="highlight-source"
title={highlight.eventReference ? 'Open on Nostr' : 'Open source'}
<div className="highlight-menu-wrapper" ref={menuRef}>
<button
className="highlight-menu-btn"
onClick={handleMenuToggle}
title="More options"
>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
)}
<FontAwesomeIcon icon={faEllipsisV} />
</button>
{showMenu && (
<div className="highlight-menu">
{sourceLink && (
<button
className="highlight-menu-item"
onClick={handleOpenExternal}
>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>{highlight.eventReference ? 'Open on Nostr' : 'Open source'}</span>
</button>
)}
{canDelete && (
<button
className="highlight-menu-item highlight-menu-item-danger"
onClick={handleMenuDeleteClick}
disabled={isDeleting}
>
<FontAwesomeIcon icon={isDeleting ? faSpinner : faTrash} spin={isDeleting} />
<span>Delete</span>
</button>
)}
</div>
)}
</div>
</div>
</div>
</div>

View File

@@ -1783,6 +1783,35 @@ body.mobile-sidebar-open {
.highlight-text {
color: #213547;
}
.highlight-menu-btn {
color: #666;
}
.highlight-menu-btn:hover {
color: #646cff;
background: rgba(100, 108, 255, 0.08);
}
.highlight-menu {
background: #fff;
border-color: #ddd;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.highlight-menu-item {
color: #213547;
}
.highlight-menu-item:hover {
background: rgba(100, 108, 255, 0.08);
color: #000;
}
.highlight-menu-item-danger:hover {
background: rgba(255, 68, 68, 0.08);
color: #cc0000;
}
}
/* Highlights Panel Styles */
@@ -2236,25 +2265,77 @@ body.mobile-sidebar-open {
line-height: 1;
}
.highlight-source {
.highlight-menu-wrapper {
position: relative;
margin-left: auto;
flex-shrink: 0;
}
.highlight-menu-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.375rem;
color: #646cff;
text-decoration: none;
transition: color 0.2s ease;
flex-shrink: 0;
margin-left: auto;
line-height: 1;
border-radius: 4px;
}
.highlight-source:hover {
color: #535bf2;
text-decoration: underline;
.highlight-menu-btn:hover {
color: #646cff;
background: rgba(100, 108, 255, 0.1);
}
.highlight-source svg {
.highlight-menu {
position: absolute;
right: 0;
top: calc(100% + 4px);
background: #2a2a2a;
border: 1px solid #444;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
min-width: 160px;
overflow: hidden;
}
.highlight-menu-item {
width: 100%;
background: none;
border: none;
color: #ddd;
padding: 0.625rem 0.875rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
transition: all 0.15s ease;
text-align: left;
white-space: nowrap;
}
.highlight-menu-item:hover {
background: rgba(100, 108, 255, 0.15);
color: #fff;
}
.highlight-menu-item:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.highlight-menu-item-danger:hover {
background: rgba(255, 68, 68, 0.15);
color: #ff4444;
}
.highlight-menu-item svg {
font-size: 0.875rem;
flex-shrink: 0;
}
/* Inline content highlights - fluorescent marker style */