feat: add view mode switching for bookmarks with compact list view

- Add ViewMode type with options: compact, cards, large
- Add view mode toggle buttons in SidebarHeader
- Implement compact list view rendering in BookmarkItem
- Add CSS styles for compact view with condensed layout
- Cards view remains the default and current style
This commit is contained in:
Gigi
2025-10-03 09:44:39 +02:00
parent 5727a38a7e
commit 99c6a4c23b
6 changed files with 221 additions and 32 deletions

View File

@@ -12,14 +12,16 @@ import { getKindIcon } from './kindIcon'
import ContentWithResolvedProfiles from './ContentWithResolvedProfiles'
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
import { classifyUrl } from '../utils/helpers'
import { ViewMode } from './Bookmarks'
interface BookmarkItemProps {
bookmark: IndividualBookmark
index: number
onSelectUrl?: (url: string) => void
viewMode?: ViewMode
}
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onSelectUrl }) => {
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onSelectUrl, viewMode = 'cards' }) => {
const [expanded, setExpanded] = useState(false)
const [urlsExpanded, setUrlsExpanded] = useState(false)
// removed copy-to-clipboard buttons
@@ -75,6 +77,46 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
// Get classification for the first URL (for the main button)
const firstUrlClassification = hasUrls ? classifyUrl(extractedUrls[0]) : null
// Compact view rendering
if (viewMode === 'compact') {
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark compact ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
<div className="compact-header">
<span className="bookmark-type-compact">
{bookmark.isPrivate ? (
<>
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
<FontAwesomeIcon icon={faUserLock} className="bookmark-visibility private" />
</>
) : (
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
)}
</span>
<div className="compact-content">
{bookmark.content && (
<div className="compact-text">
<ContentWithResolvedProfiles content={bookmark.content.slice(0, 100) + (bookmark.content.length > 100 ? '…' : '')} />
</div>
)}
<div className="compact-meta">
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>
{hasUrls && (
<button
className="compact-read-btn"
onClick={(e) => { e.preventDefault(); onSelectUrl?.(extractedUrls[0]) }}
title={firstUrlClassification?.buttonText}
>
<FontAwesomeIcon icon={getIconForUrlType(extractedUrls[0])} />
</button>
)}
</div>
</div>
</div>
</div>
)
}
// Card/Large view rendering (existing)
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
<div className="bookmark-header">

View File

@@ -5,6 +5,7 @@ import { Bookmark } from '../types/bookmarks'
import { BookmarkItem } from './BookmarkItem'
import { formatDate, renderParsedContent } from '../utils/bookmarkUtils'
import SidebarHeader from './SidebarHeader'
import { ViewMode } from './Bookmarks'
interface BookmarkListProps {
bookmarks: Bookmark[]
@@ -12,6 +13,8 @@ interface BookmarkListProps {
isCollapsed: boolean
onToggleCollapse: () => void
onLogout: () => void
viewMode: ViewMode
onViewModeChange: (mode: ViewMode) => void
}
export const BookmarkList: React.FC<BookmarkListProps> = ({
@@ -19,7 +22,9 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
onSelectUrl,
isCollapsed,
onToggleCollapse,
onLogout
onLogout,
viewMode,
onViewModeChange
}) => {
if (isCollapsed) {
return (
@@ -38,7 +43,12 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
return (
<div className="bookmarks-container">
<SidebarHeader onToggleCollapse={onToggleCollapse} onLogout={onLogout} />
<SidebarHeader
onToggleCollapse={onToggleCollapse}
onLogout={onLogout}
viewMode={viewMode}
onViewModeChange={onViewModeChange}
/>
{bookmarks.length === 0 ? (
<div className="empty-state">
@@ -75,9 +85,15 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
)}
{bookmark.individualBookmarks && bookmark.individualBookmarks.length > 0 && (
<div className="individual-bookmarks">
<div className="bookmarks-grid">
<div className={`bookmarks-grid bookmarks-${viewMode}`}>
{bookmark.individualBookmarks.map((individualBookmark, index) =>
<BookmarkItem key={index} bookmark={individualBookmark} index={index} onSelectUrl={onSelectUrl} />
<BookmarkItem
key={index}
bookmark={individualBookmark}
index={index}
onSelectUrl={onSelectUrl}
viewMode={viewMode}
/>
)}
</div>
</div>

View File

@@ -7,6 +7,8 @@ import { fetchBookmarks } from '../services/bookmarkService'
import ContentPanel from './ContentPanel'
import { fetchReadableContent, ReadableContent } from '../services/readerService'
export type ViewMode = 'compact' | 'cards' | 'large'
interface BookmarksProps {
relayPool: RelayPool | null
onLogout: () => void
@@ -19,6 +21,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
const [readerLoading, setReaderLoading] = useState(false)
const [readerContent, setReaderContent] = useState<ReadableContent | undefined>(undefined)
const [isCollapsed, setIsCollapsed] = useState(false)
const [viewMode, setViewMode] = useState<ViewMode>('cards')
const activeAccount = Hooks.useActiveAccount()
const accountManager = Hooks.useAccountManager()
@@ -85,6 +88,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
onLogout={onLogout}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
</div>
<div className="pane main">

View File

@@ -1,17 +1,20 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faRightFromBracket, faUser } from '@fortawesome/free-solid-svg-icons'
import { faChevronRight, faRightFromBracket, faUser, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons'
import { Hooks } from 'applesauce-react'
import { useEventModel } from 'applesauce-react/hooks'
import { Models } from 'applesauce-core'
import IconButton from './IconButton'
import { ViewMode } from './Bookmarks'
interface SidebarHeaderProps {
onToggleCollapse: () => void
onLogout: () => void
viewMode: ViewMode
onViewModeChange: (mode: ViewMode) => void
}
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogout }) => {
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogout, viewMode, onViewModeChange }) => {
const activeAccount = Hooks.useActiveAccount()
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
@@ -30,30 +33,55 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
const profileImage = getProfileImage()
return (
<div className="sidebar-header-bar">
<button
onClick={onToggleCollapse}
className="toggle-sidebar-btn"
title="Collapse bookmarks sidebar"
aria-label="Collapse bookmarks sidebar"
>
<FontAwesomeIcon icon={faChevronRight} />
</button>
<div className="profile-avatar" title={getUserDisplayName()}>
{profileImage ? (
<img src={profileImage} alt={getUserDisplayName()} />
) : (
<FontAwesomeIcon icon={faUser} />
)}
<>
<div className="sidebar-header-bar">
<button
onClick={onToggleCollapse}
className="toggle-sidebar-btn"
title="Collapse bookmarks sidebar"
aria-label="Collapse bookmarks sidebar"
>
<FontAwesomeIcon icon={faChevronRight} />
</button>
<div className="profile-avatar" title={getUserDisplayName()}>
{profileImage ? (
<img src={profileImage} alt={getUserDisplayName()} />
) : (
<FontAwesomeIcon icon={faUser} />
)}
</div>
<IconButton
icon={faRightFromBracket}
onClick={onLogout}
title="Logout"
ariaLabel="Logout"
variant="ghost"
/>
</div>
<IconButton
icon={faRightFromBracket}
onClick={onLogout}
title="Logout"
ariaLabel="Logout"
variant="ghost"
/>
</div>
<div className="view-mode-controls">
<IconButton
icon={faList}
onClick={() => onViewModeChange('compact')}
title="Compact list view"
ariaLabel="Compact list view"
variant={viewMode === 'compact' ? 'primary' : 'ghost'}
/>
<IconButton
icon={faThLarge}
onClick={() => onViewModeChange('cards')}
title="Cards view"
ariaLabel="Cards view"
variant={viewMode === 'cards' ? 'primary' : 'ghost'}
/>
<IconButton
icon={faImage}
onClick={() => onViewModeChange('large')}
title="Large preview view"
ariaLabel="Large preview view"
variant={viewMode === 'large' ? 'primary' : 'ghost'}
/>
</div>
</>
)
}

View File

@@ -107,7 +107,19 @@ body {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.view-mode-controls {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
margin-bottom: 1rem;
justify-content: center;
}
.profile-avatar {
@@ -588,6 +600,10 @@ body {
gap: 1rem;
}
.bookmarks-grid.bookmarks-compact {
gap: 0.5rem;
}
.individual-bookmark {
background: #2a2a2a;
padding: 1.25rem;
@@ -605,6 +621,88 @@ body {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Compact view styles */
.individual-bookmark.compact {
padding: 0.5rem 0.75rem;
background: transparent;
border-bottom: 1px solid #333;
border-radius: 0;
box-shadow: none;
}
.individual-bookmark.compact:hover {
background: #2a2a2a;
transform: none;
box-shadow: none;
}
.compact-header {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.bookmark-type-compact {
display: flex;
align-items: center;
gap: 0.25rem;
color: #646cff;
font-size: 0.9rem;
flex-shrink: 0;
padding-top: 0.25rem;
}
.compact-content {
flex: 1;
min-width: 0;
}
.compact-text {
color: #ccc;
font-size: 0.9rem;
line-height: 1.4;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.compact-meta {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
}
.bookmark-date-compact {
font-size: 0.75rem;
color: #666;
}
.compact-read-btn {
background: #28a745;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 24px;
transition: background-color 0.2s ease;
}
.compact-read-btn:hover {
background: #218838;
}
.compact-read-btn:active {
transform: translateY(1px);
}
.bookmark-header {
display: flex;
justify-content: space-between;