diff --git a/src/components/BookmarkFilters.tsx b/src/components/BookmarkFilters.tsx new file mode 100644 index 00000000..0091852d --- /dev/null +++ b/src/components/BookmarkFilters.tsx @@ -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 = ({ + 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 ( +
+ {filters.map(filter => ( + + ))} +
+ ) +} + +export default BookmarkFilters + diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index c487a216..74e6adc5 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -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 = ({ const bookmarksListRef = useRef(null) const friendsColor = settings?.highlightColorFriends || '#f97316' const [showAddModal, setShowAddModal] = useState(false) + const [selectedFilter, setSelectedFilter] = useState('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 = ({ 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 = ({ isMobile={isMobile} /> - {allIndividualBookmarks.length === 0 ? ( + {allIndividualBookmarks.length > 0 && ( + + )} + + {filteredBookmarks.length === 0 && allIndividualBookmarks.length > 0 ? ( +
+

No bookmarks match this filter.

+
+ ) : allIndividualBookmarks.length === 0 ? ( loading ? (
diff --git a/src/styles/layout/sidebar.css b/src/styles/layout/sidebar.css index 2013ed7c..0014d99d 100644 --- a/src/styles/layout/sidebar.css +++ b/src/styles/layout/sidebar.css @@ -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); +} + diff --git a/src/utils/bookmarkTypeClassifier.ts b/src/utils/bookmarkTypeClassifier.ts new file mode 100644 index 00000000..fe6096e8 --- /dev/null +++ b/src/utils/bookmarkTypeClassifier.ts @@ -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) +} +