mirror of
https://github.com/dergigi/boris.git
synced 2025-12-26 02:54:29 +01:00
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:
43
src/components/BookmarkFilters.tsx
Normal file
43
src/components/BookmarkFilters.tsx
Normal 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
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
41
src/utils/bookmarkTypeClassifier.ts
Normal file
41
src/utils/bookmarkTypeClassifier.ts
Normal 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user