From 0cf7f93482a4a4f39bfd5f98477423d83e80e699 Mon Sep 17 00:00:00 2001 From: Gigi Date: Fri, 3 Oct 2025 10:29:17 +0200 Subject: [PATCH] refactor: split BookmarkItem into separate view components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract CompactView, LargeView, and CardView into separate files - Keep all files under 210 lines (BookmarkItem: 307→105 lines) - Improve code organization and maintainability - Add shared type definitions for view components - Keep DRY with shared props object --- src/components/BookmarkItem.tsx | 245 ++----------------- src/components/BookmarkViews/CardView.tsx | 153 ++++++++++++ src/components/BookmarkViews/CompactView.tsx | 71 ++++++ src/components/BookmarkViews/LargeView.tsx | 94 +++++++ src/components/BookmarkViews/shared.ts | 4 + 5 files changed, 344 insertions(+), 223 deletions(-) create mode 100644 src/components/BookmarkViews/CardView.tsx create mode 100644 src/components/BookmarkViews/CompactView.tsx create mode 100644 src/components/BookmarkViews/LargeView.tsx create mode 100644 src/components/BookmarkViews/shared.ts diff --git a/src/components/BookmarkItem.tsx b/src/components/BookmarkItem.tsx index 6423d38b..c82ce4de 100644 --- a/src/components/BookmarkItem.tsx +++ b/src/components/BookmarkItem.tsx @@ -1,18 +1,16 @@ import React, { useState } from 'react' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons' -import { faChevronDown, faChevronUp, faBookOpen, faPlay, faEye } from '@fortawesome/free-solid-svg-icons' -import IconButton from './IconButton' +import { faBookOpen, faPlay, faEye } from '@fortawesome/free-solid-svg-icons' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' import { npubEncode, neventEncode } from 'nostr-tools/nip19' import { IndividualBookmark } from '../types/bookmarks' -import { formatDate, renderParsedContent } from '../utils/bookmarkUtils' -import ContentWithResolvedProfiles from './ContentWithResolvedProfiles' import { extractUrlsFromContent } from '../services/bookmarkHelpers' import { classifyUrl } from '../utils/helpers' import { ViewMode } from './Bookmarks' import { getPreviewImage, fetchOgImage } from '../utils/imagePreview' +import { CompactView } from './BookmarkViews/CompactView' +import { LargeView } from './BookmarkViews/LargeView' +import { CardView } from './BookmarkViews/CardView' interface BookmarkItemProps { bookmark: IndividualBookmark @@ -22,10 +20,7 @@ interface BookmarkItemProps { } export const BookmarkItem: React.FC = ({ bookmark, index, onSelectUrl, viewMode = 'cards' }) => { - const [expanded, setExpanded] = useState(false) - const [urlsExpanded, setUrlsExpanded] = useState(false) const [ogImage, setOgImage] = useState(null) - // removed copy-to-clipboard buttons const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}` @@ -42,8 +37,6 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS fetchOgImage(firstUrl).then(setOgImage) } }, [viewMode, firstUrl, instantPreview, ogImage]) - const contentLength = (bookmark.content || '').length - const shouldTruncate = !expanded && contentLength > 210 // Resolve author profile using applesauce const authorProfile = useEventModel(Models.ProfileModel, [bookmark.pubkey]) @@ -85,222 +78,28 @@ export const BookmarkItem: React.FC = ({ bookmark, index, onS } } - // Compact view rendering - if (viewMode === 'compact') { - const handleCompactClick = () => { - if (hasUrls && onSelectUrl) { - onSelectUrl(extractedUrls[0]) - } - } - - return ( -
-
- - {bookmark.isPrivate ? ( - <> - - - - ) : ( - - )} - - {bookmark.content && ( -
- 60 ? '…' : '')} /> -
- )} - {formatDate(bookmark.created_at)} - {hasUrls && ( - - )} -
-
- ) + const sharedProps = { + bookmark, + index, + hasUrls, + extractedUrls, + onSelectUrl, + getIconForUrlType, + firstUrlClassification, + authorNpub, + eventNevent, + getAuthorDisplayName, + handleReadNow + } + + if (viewMode === 'compact') { + return } - // Large preview view rendering if (viewMode === 'large') { const previewImage = instantPreview || ogImage - - return ( -
- {hasUrls && ( -
onSelectUrl?.(extractedUrls[0])} - style={previewImage ? { backgroundImage: `url(${previewImage})` } : undefined} - > - {!previewImage && ( -
- -
- )} -
- )} - -
- {bookmark.content && ( -
- -
- )} - -
- - - {getAuthorDisplayName()} - - - - {eventNevent && ( - - {formatDate(bookmark.created_at)} - - )} - - {hasUrls && firstUrlClassification && ( - - )} -
-
-
- ) + return } - // Card view rendering (default) - return ( -
-
- - {bookmark.isPrivate ? ( - <> - - - - ) : ( - - )} - - - {eventNevent ? ( - - {formatDate(bookmark.created_at)} - - ) : ( - {formatDate(bookmark.created_at)} - )} -
- - {extractedUrls.length > 0 && ( -
- {(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 1)).map((url, urlIndex) => { - const classification = classifyUrl(url) - return ( -
- - { e.preventDefault(); onSelectUrl?.(url) }} - /> -
- ) - })} - {extractedUrls.length > 1 && ( - - )} -
- )} - - {bookmark.parsedContent ? ( -
- {shouldTruncate && bookmark.content - ? - : renderParsedContent(bookmark.parsedContent)} -
- ) : bookmark.content && ( -
- -
- )} - - {contentLength > 210 && ( - - )} - -
- - {hasUrls && firstUrlClassification && ( - - )} -
-
- ) + return } diff --git a/src/components/BookmarkViews/CardView.tsx b/src/components/BookmarkViews/CardView.tsx new file mode 100644 index 00000000..278e5ce8 --- /dev/null +++ b/src/components/BookmarkViews/CardView.tsx @@ -0,0 +1,153 @@ +import React, { useState } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBookmark, faUserLock, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons' +import { IndividualBookmark } from '../../types/bookmarks' +import { formatDate, renderParsedContent } from '../../utils/bookmarkUtils' +import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' +import IconButton from '../IconButton' +import { classifyUrl } from '../../utils/helpers' +import { IconGetter } from './shared' + +interface CardViewProps { + bookmark: IndividualBookmark + index: number + hasUrls: boolean + extractedUrls: string[] + onSelectUrl?: (url: string) => void + getIconForUrlType: IconGetter + firstUrlClassification: { buttonText: string } | null + authorNpub: string + eventNevent?: string + getAuthorDisplayName: () => string + handleReadNow: (e: React.MouseEvent) => void +} + +export const CardView: React.FC = ({ + bookmark, + index, + hasUrls, + extractedUrls, + onSelectUrl, + getIconForUrlType, + firstUrlClassification, + authorNpub, + eventNevent, + getAuthorDisplayName, + handleReadNow +}) => { + const [expanded, setExpanded] = useState(false) + const [urlsExpanded, setUrlsExpanded] = useState(false) + const contentLength = (bookmark.content || '').length + const shouldTruncate = !expanded && contentLength > 210 + + return ( +
+
+ + {bookmark.isPrivate ? ( + <> + + + + ) : ( + + )} + + + {eventNevent ? ( + + {formatDate(bookmark.created_at)} + + ) : ( + {formatDate(bookmark.created_at)} + )} +
+ + {extractedUrls.length > 0 && ( +
+ {(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 1)).map((url, urlIndex) => { + const classification = classifyUrl(url) + return ( +
+ + { e.preventDefault(); onSelectUrl?.(url) }} + /> +
+ ) + })} + {extractedUrls.length > 1 && ( + + )} +
+ )} + + {bookmark.parsedContent ? ( +
+ {shouldTruncate && bookmark.content + ? + : renderParsedContent(bookmark.parsedContent)} +
+ ) : bookmark.content && ( +
+ +
+ )} + + {contentLength > 210 && ( + + )} + +
+ + {hasUrls && firstUrlClassification && ( + + )} +
+
+ ) +} + diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx new file mode 100644 index 00000000..0be7301f --- /dev/null +++ b/src/components/BookmarkViews/CompactView.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons' +import { IndividualBookmark } from '../../types/bookmarks' +import { formatDate } from '../../utils/bookmarkUtils' +import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' +import { IconGetter } from './shared' + +interface CompactViewProps { + bookmark: IndividualBookmark + index: number + hasUrls: boolean + extractedUrls: string[] + onSelectUrl?: (url: string) => void + getIconForUrlType: IconGetter + firstUrlClassification: { buttonText: string } | null +} + +export const CompactView: React.FC = ({ + bookmark, + index, + hasUrls, + extractedUrls, + onSelectUrl, + getIconForUrlType, + firstUrlClassification +}) => { + const handleCompactClick = () => { + if (hasUrls && onSelectUrl) { + onSelectUrl(extractedUrls[0]) + } + } + + return ( +
+
+ + {bookmark.isPrivate ? ( + <> + + + + ) : ( + + )} + + {bookmark.content && ( +
+ 60 ? '…' : '')} /> +
+ )} + {formatDate(bookmark.created_at)} + {hasUrls && ( + + )} +
+
+ ) +} + diff --git a/src/components/BookmarkViews/LargeView.tsx b/src/components/BookmarkViews/LargeView.tsx new file mode 100644 index 00000000..c0c1b33c --- /dev/null +++ b/src/components/BookmarkViews/LargeView.tsx @@ -0,0 +1,94 @@ +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { IndividualBookmark } from '../../types/bookmarks' +import { formatDate } from '../../utils/bookmarkUtils' +import ContentWithResolvedProfiles from '../ContentWithResolvedProfiles' +import { IconGetter } from './shared' + +interface LargeViewProps { + bookmark: IndividualBookmark + index: number + hasUrls: boolean + extractedUrls: string[] + onSelectUrl?: (url: string) => void + getIconForUrlType: IconGetter + firstUrlClassification: { buttonText: string } | null + previewImage: string | null + authorNpub: string + eventNevent?: string + getAuthorDisplayName: () => string + handleReadNow: (e: React.MouseEvent) => void +} + +export const LargeView: React.FC = ({ + bookmark, + index, + hasUrls, + extractedUrls, + onSelectUrl, + getIconForUrlType, + firstUrlClassification, + previewImage, + authorNpub, + eventNevent, + getAuthorDisplayName, + handleReadNow +}) => { + return ( +
+ {hasUrls && ( +
onSelectUrl?.(extractedUrls[0])} + style={previewImage ? { backgroundImage: `url(${previewImage})` } : undefined} + > + {!previewImage && ( +
+ +
+ )} +
+ )} + +
+ {bookmark.content && ( +
+ +
+ )} + +
+ + + {getAuthorDisplayName()} + + + + {eventNevent && ( + + {formatDate(bookmark.created_at)} + + )} + + {hasUrls && firstUrlClassification && ( + + )} +
+
+
+ ) +} + diff --git a/src/components/BookmarkViews/shared.ts b/src/components/BookmarkViews/shared.ts new file mode 100644 index 00000000..58568d21 --- /dev/null +++ b/src/components/BookmarkViews/shared.ts @@ -0,0 +1,4 @@ +import { IconDefinition } from '@fortawesome/fontawesome-svg-core' + +export type IconGetter = (url: string) => IconDefinition +