diff --git a/FEATURES.md b/FEATURES.md index 08c99e62..4a2a1f2c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -37,10 +37,17 @@ ## Explore & Profiles -- **Explore**: Discover friends’ highlights and writings, plus a “nostrverse” feed. +- **Explore**: Discover friends' highlights and writings, plus a "nostrverse" feed. - **Filters**: Visibility toggles (mine, friends, nostrverse) apply to Explore highlights. - **Profiles**: View your own (`/me`) or other users (`/p/:npub`) with tabs for Highlights, Bookmarks, Archive, and Writings. +## Support + +- **Supporter page**: Displays avatars of users who zapped Boris (kind:9735 receipts). +- **Thresholds**: Shows supporters who sent ≥ 2100 sats; whales (≥ 69420 sats) get special styling with a bolt badge. +- **Profile integration**: Fetches and displays profile pictures and names for all supporters. +- **Stats**: Total supporter count and zap count displayed at the bottom. + ## Video - **Embedded player**: Plays supported videos (e.g., YouTube) inline with duration display. diff --git a/public/thank-you.svg b/public/thank-you.svg new file mode 100644 index 00000000..c1f4ab26 --- /dev/null +++ b/public/thank-you.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0a0c6e7f..68486c89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,6 +62,15 @@ function AppRoutes({ /> } /> + + } + /> = ({ relayPool, onLogout }) => { const showExplore = location.pathname.startsWith('/explore') const showMe = location.pathname.startsWith('/me') const showProfile = location.pathname.startsWith('/p/') + const showSupport = location.pathname === '/support' // Extract tab from explore routes const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights' @@ -250,6 +252,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { showExplore={showExplore} showMe={showMe} showProfile={showProfile} + showSupport={showSupport} bookmarks={bookmarks} bookmarksLoading={bookmarksLoading} viewMode={viewMode} @@ -313,6 +316,9 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { profile={showProfile && profilePubkey ? ( relayPool ? : null ) : undefined} + support={showSupport ? ( + relayPool ? : null + ) : undefined} toastMessage={toastMessage ?? undefined} toastType={toastType} onClearToast={clearToast} diff --git a/src/components/SidebarHeader.tsx b/src/components/SidebarHeader.tsx index d326eb8f..28a99abe 100644 --- a/src/components/SidebarHeader.tsx +++ b/src/components/SidebarHeader.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faHome, faPlus, faNewspaper, faTimes } from '@fortawesome/free-solid-svg-icons' +import { faChevronRight, faRightFromBracket, faRightToBracket, faUserCircle, faGear, faHome, faPlus, faNewspaper, faTimes, faBolt } from '@fortawesome/free-solid-svg-icons' import { Hooks } from 'applesauce-react' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' @@ -117,6 +117,13 @@ const SidebarHeader: React.FC = ({ onToggleCollapse, onLogou ariaLabel="Explore" variant="ghost" /> + navigate('/support')} + title="Support" + ariaLabel="Support" + variant="ghost" + /> = ({ relayPool, eventStore, settings }) => { + const [supporters, setSupporters] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + const loadSupporters = async () => { + setLoading(true) + try { + const zappers = await fetchBorisZappers(relayPool) + + if (zappers.length > 0) { + const pubkeys = zappers.map(z => z.pubkey) + await fetchProfiles(relayPool, eventStore, pubkeys, settings) + } + + setSupporters(zappers) + } catch (error) { + console.error('Failed to load supporters:', error) + } finally { + setLoading(false) + } + } + + loadSupporters() + }, [relayPool, eventStore, settings]) + + if (loading) { + return ( +
+ +
+ ) + } + + return ( +
+
+
+
+ Thank you +
+

+ Thank You! +

+

+ Your{' '} + + zaps + + {' '}help keep this project alive. +

+
+ + {supporters.length === 0 ? ( +
+

No supporters yet. Be the first to zap Boris!

+
+ ) : ( + <> + {/* Whales Section */} + {supporters.filter(s => s.isWhale).length > 0 && ( +
+

+ Legends +

+
+ {supporters.filter(s => s.isWhale).map(supporter => ( + + ))} +
+
+ )} + + {/* Regular Supporters Section */} + {supporters.filter(s => !s.isWhale).length > 0 && ( +
+

+ Supporters +

+
+ {supporters.filter(s => !s.isWhale).map(supporter => ( + + ))} +
+
+ )} + + )} + +
+
+

+ Zap{' '} + + Boris + + {' '}a{' '} + + meaningful amount of sats + + {' '}and your avatar will show above. +

+

+ Total supporters: {supporters.length} • + Total zaps: {supporters.reduce((sum, s) => sum + s.zapCount, 0)} +

+
+
+
+
+ ) +} + +interface SupporterCardProps { + supporter: SupporterProfile + isWhale: boolean +} + +const SupporterCard: React.FC = ({ supporter, isWhale }) => { + const navigate = useNavigate() + const profile = useEventModel(Models.ProfileModel, [supporter.pubkey]) + + const picture = profile?.picture + const name = profile?.name || profile?.display_name || `${supporter.pubkey.slice(0, 8)}...` + + const handleClick = () => { + const npub = nip19.npubEncode(supporter.pubkey) + navigate(`/p/${npub}`) + } + + return ( +
+
+ {/* Avatar */} +
+ {picture ? ( + {name} + ) : ( + + )} +
+ + {/* Whale Badge */} + {isWhale && ( +
+ +
+ )} +
+ + {/* Name and Total */} +
+

+ {name} +

+

+ {supporter.totalSats.toLocaleString()} sats +

+
+
+ ) +} + +export default Support + diff --git a/src/components/ThreePaneLayout.tsx b/src/components/ThreePaneLayout.tsx index 8c115cec..016f3650 100644 --- a/src/components/ThreePaneLayout.tsx +++ b/src/components/ThreePaneLayout.tsx @@ -32,6 +32,7 @@ interface ThreePaneLayoutProps { showExplore?: boolean showMe?: boolean showProfile?: boolean + showSupport?: boolean // Bookmarks pane bookmarks: Bookmark[] @@ -93,6 +94,9 @@ interface ThreePaneLayoutProps { // Optional Profile content profile?: React.ReactNode + + // Optional Support content + support?: React.ReactNode } const ThreePaneLayout: React.FC = (props) => { @@ -225,8 +229,8 @@ const ThreePaneLayout: React.FC = (props) => { return ( <> - {/* Mobile bookmark button - only show when viewing article (not on settings/explore/me/profile) */} - {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && ( + {/* Mobile bookmark button - only show when viewing article (not on settings/explore/me/profile/support) */} + {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && !props.showSupport && ( )} - {/* Mobile highlights button - only show when viewing article (not on settings/explore/me/profile) */} - {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && ( + {/* Mobile highlights button - only show when viewing article (not on settings/explore/me/profile/support) */} + {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && !props.showSupport && (