feat: add bookmark filter buttons by content type

- Add BookmarkFilters component with icon-based filter buttons
- Create bookmarkTypeClassifier utility for content type classification
- Filter bookmarks by article, video, note, or web types
- Apply filters across all bookmark lists (private, public, web, sets)
- Style filter buttons to match existing UI design
This commit is contained in:
Gigi
2025-10-15 21:17:27 +02:00
parent a215d0b026
commit ee1365d3ca
4 changed files with 143 additions and 3 deletions

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faNewspaper, faStickyNote, faCirclePlay } from '@fortawesome/free-regular-svg-icons'
import { faGlobe, faAsterisk } from '@fortawesome/free-solid-svg-icons'
export type BookmarkFilterType = 'all' | 'article' | 'video' | 'note' | 'web'
interface BookmarkFiltersProps {
selectedFilter: BookmarkFilterType
onFilterChange: (filter: BookmarkFilterType) => void
}
const BookmarkFilters: React.FC<BookmarkFiltersProps> = ({
selectedFilter,
onFilterChange
}) => {
const filters = [
{ type: 'all' as const, icon: faAsterisk, label: 'All' },
{ type: 'article' as const, icon: faNewspaper, label: 'Articles' },
{ type: 'video' as const, icon: faCirclePlay, label: 'Videos' },
{ type: 'note' as const, icon: faStickyNote, label: 'Notes' },
{ type: 'web' as const, icon: faGlobe, label: 'Web' }
]
return (
<div className="bookmark-filters">
{filters.map(filter => (
<button
key={filter.type}
onClick={() => onFilterChange(filter.type)}
className={`filter-btn ${selectedFilter === filter.type ? 'active' : ''}`}
title={filter.label}
aria-label={`Filter by ${filter.label}`}
>
<FontAwesomeIcon icon={filter.icon} />
</button>
))}
</div>
)
}
export default BookmarkFilters

View File

@@ -19,6 +19,8 @@ import AddBookmarkModal from './AddBookmarkModal'
import { createWebBookmark } from '../services/webBookmarkService'
import { RELAYS } from '../config/relays'
import { Hooks } from 'applesauce-react'
import BookmarkFilters, { BookmarkFilterType } from './BookmarkFilters'
import { filterBookmarksByType } from '../utils/bookmarkTypeClassifier'
interface BookmarkListProps {
bookmarks: Bookmark[]
@@ -61,6 +63,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
const bookmarksListRef = useRef<HTMLDivElement>(null)
const friendsColor = settings?.highlightColorFriends || '#f97316'
const [showAddModal, setShowAddModal] = useState(false)
const [selectedFilter, setSelectedFilter] = useState<BookmarkFilterType>('all')
const activeAccount = Hooks.useActiveAccount()
const handleSaveBookmark = async (url: string, title?: string, description?: string, tags?: string[]) => {
@@ -87,9 +90,12 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
const allIndividualBookmarks = bookmarks.flatMap(b => b.individualBookmarks || [])
.filter(hasContent)
// Apply filter
const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter)
// Separate bookmarks with setName (kind 30003) from regular bookmarks
const bookmarksWithoutSet = getBookmarksWithoutSet(allIndividualBookmarks)
const bookmarkSets = getBookmarkSets(allIndividualBookmarks)
const bookmarksWithoutSet = getBookmarksWithoutSet(filteredBookmarks)
const bookmarkSets = getBookmarkSets(filteredBookmarks)
// Group non-set bookmarks as before
const groups = groupIndividualBookmarks(bookmarksWithoutSet)
@@ -140,7 +146,18 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
isMobile={isMobile}
/>
{allIndividualBookmarks.length === 0 ? (
{allIndividualBookmarks.length > 0 && (
<BookmarkFilters
selectedFilter={selectedFilter}
onFilterChange={setSelectedFilter}
/>
)}
{filteredBookmarks.length === 0 && allIndividualBookmarks.length > 0 ? (
<div className="empty-state">
<p>No bookmarks match this filter.</p>
</div>
) : allIndividualBookmarks.length === 0 ? (
loading ? (
<div className={`bookmarks-list ${viewMode}`} aria-busy="true">
<div className={`bookmarks-grid bookmarks-${viewMode}`}>

View File

@@ -176,3 +176,42 @@
.read-inline-btn { background: rgb(34 197 94); /* green-500 */ color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; }
.read-inline-btn:hover { background: rgb(22 163 74); /* green-600 */ }
/* Bookmark filters */
.bookmark-filters {
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
}
.bookmark-filters .filter-btn {
background: transparent;
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.bookmark-filters .filter-btn:hover {
background: var(--color-bg-elevated);
color: var(--color-text);
border-color: var(--color-border);
}
.bookmark-filters .filter-btn.active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.bookmark-filters .filter-btn:active {
transform: translateY(1px);
}

View File

@@ -0,0 +1,41 @@
import { IndividualBookmark } from '../types/bookmarks'
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
import { classifyUrl } from './helpers'
export type BookmarkType = 'article' | 'video' | 'note' | 'web'
/**
* Classifies a bookmark into one of the content types
*/
export function classifyBookmarkType(bookmark: IndividualBookmark): BookmarkType {
if (bookmark.kind === 30023) return 'article'
const isWebBookmark = bookmark.kind === 39701
const webBookmarkUrl = isWebBookmark ? bookmark.tags.find(t => t[0] === 'd')?.[1] : null
const extractedUrls = webBookmarkUrl
? [webBookmarkUrl.startsWith('http') ? webBookmarkUrl : `https://${webBookmarkUrl}`]
: extractUrlsFromContent(bookmark.content)
const firstUrl = extractedUrls[0]
if (!firstUrl) return 'note'
const urlType = classifyUrl(firstUrl)?.type
if (urlType === 'youtube' || urlType === 'video') return 'video'
if (urlType === 'article') return 'article'
return 'web'
}
/**
* Filters bookmarks by type
*/
export function filterBookmarksByType(
bookmarks: IndividualBookmark[],
filterType: 'all' | BookmarkType
): IndividualBookmark[] {
if (filterType === 'all') return bookmarks
return bookmarks.filter(bookmark => classifyBookmarkType(bookmark) === filterType)
}