mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 15:14:20 +01:00
feat: add public profile pages at /p/:npub
- Make AuthorCard clickable to navigate to user profiles - Add /p/:npub and /p/:npub/writings routes - Reuse Me component for public profiles with pubkey prop - Show highlights and writings tabs for any user - Hide private tabs (reading-list, archive) on public profiles - Public profiles show only public data (highlights, writings) - Private data (bookmarks, read articles) only visible on own profile - Add clickable author card hover styles with indigo border - Decode npub to pubkey for profile viewing - DRY: Single Me component serves both /me and /p/:npub routes
This commit is contained in:
18
src/App.tsx
18
src/App.tsx
@@ -110,6 +110,24 @@ function AppRoutes({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/p/:npub"
|
||||||
|
element={
|
||||||
|
<Bookmarks
|
||||||
|
relayPool={relayPool}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/p/:npub/writings"
|
||||||
|
element={
|
||||||
|
<Bookmarks
|
||||||
|
relayPool={relayPool}
|
||||||
|
onLogout={handleLogout}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'
|
import { faUserCircle } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useEventModel } from 'applesauce-react/hooks'
|
import { useEventModel } from 'applesauce-react/hooks'
|
||||||
import { Models } from 'applesauce-core'
|
import { Models } from 'applesauce-core'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
authorPubkey: string
|
authorPubkey: string
|
||||||
|
clickable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey }) => {
|
const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey, clickable = true }) => {
|
||||||
|
const navigate = useNavigate()
|
||||||
const profile = useEventModel(Models.ProfileModel, [authorPubkey])
|
const profile = useEventModel(Models.ProfileModel, [authorPubkey])
|
||||||
|
|
||||||
const getAuthorName = () => {
|
const getAuthorName = () => {
|
||||||
@@ -20,8 +24,19 @@ const AuthorCard: React.FC<AuthorCardProps> = ({ authorPubkey }) => {
|
|||||||
const authorImage = profile?.picture || profile?.image
|
const authorImage = profile?.picture || profile?.image
|
||||||
const authorBio = profile?.about
|
const authorBio = profile?.about
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (clickable) {
|
||||||
|
const npub = nip19.npubEncode(authorPubkey)
|
||||||
|
navigate(`/p/${npub}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="author-card">
|
<div
|
||||||
|
className={`author-card ${clickable ? 'author-card-clickable' : ''}`}
|
||||||
|
onClick={handleClick}
|
||||||
|
style={clickable ? { cursor: 'pointer' } : undefined}
|
||||||
|
>
|
||||||
<div className="author-card-avatar">
|
<div className="author-card-avatar">
|
||||||
{authorImage ? (
|
{authorImage ? (
|
||||||
<img src={authorImage} alt={getAuthorName()} />
|
<img src={authorImage} alt={getAuthorName()} />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useParams, useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import { Hooks } from 'applesauce-react'
|
import { Hooks } from 'applesauce-react'
|
||||||
import { useEventStore } from 'applesauce-react/hooks'
|
import { useEventStore } from 'applesauce-react/hooks'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
import { useSettings } from '../hooks/useSettings'
|
import { useSettings } from '../hooks/useSettings'
|
||||||
import { useArticleLoader } from '../hooks/useArticleLoader'
|
import { useArticleLoader } from '../hooks/useArticleLoader'
|
||||||
import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
|
||||||
@@ -25,7 +26,7 @@ interface BookmarksProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||||
const { naddr } = useParams<{ naddr?: string }>()
|
const { naddr, npub } = useParams<{ naddr?: string; npub?: string }>()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const previousLocationRef = useRef<string>()
|
const previousLocationRef = useRef<string>()
|
||||||
@@ -37,6 +38,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
const showSettings = location.pathname === '/settings'
|
const showSettings = location.pathname === '/settings'
|
||||||
const showExplore = location.pathname === '/explore'
|
const showExplore = location.pathname === '/explore'
|
||||||
const showMe = location.pathname.startsWith('/me')
|
const showMe = location.pathname.startsWith('/me')
|
||||||
|
const showProfile = location.pathname.startsWith('/p/')
|
||||||
|
|
||||||
// Extract tab from me routes
|
// Extract tab from me routes
|
||||||
const meTab = location.pathname === '/me' ? 'highlights' :
|
const meTab = location.pathname === '/me' ? 'highlights' :
|
||||||
@@ -45,12 +47,28 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
location.pathname === '/me/archive' ? 'archive' :
|
location.pathname === '/me/archive' ? 'archive' :
|
||||||
location.pathname === '/me/writings' ? 'writings' : 'highlights'
|
location.pathname === '/me/writings' ? 'writings' : 'highlights'
|
||||||
|
|
||||||
// Track previous location for going back from settings/me/explore
|
// Extract tab from profile routes
|
||||||
|
const profileTab = location.pathname.endsWith('/writings') ? 'writings' : 'highlights'
|
||||||
|
|
||||||
|
// Decode npub to pubkey for profile view
|
||||||
|
let profilePubkey: string | undefined
|
||||||
|
if (npub && showProfile) {
|
||||||
|
try {
|
||||||
|
const decoded = nip19.decode(npub)
|
||||||
|
if (decoded.type === 'npub') {
|
||||||
|
profilePubkey = decoded.data
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to decode npub:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track previous location for going back from settings/me/explore/profile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSettings && !showMe && !showExplore) {
|
if (!showSettings && !showMe && !showExplore && !showProfile) {
|
||||||
previousLocationRef.current = location.pathname
|
previousLocationRef.current = location.pathname
|
||||||
}
|
}
|
||||||
}, [location.pathname, showSettings, showMe, showExplore])
|
}, [location.pathname, showSettings, showMe, showExplore, showProfile])
|
||||||
|
|
||||||
const activeAccount = Hooks.useActiveAccount()
|
const activeAccount = Hooks.useActiveAccount()
|
||||||
const accountManager = Hooks.useAccountManager()
|
const accountManager = Hooks.useAccountManager()
|
||||||
@@ -212,6 +230,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
showSettings={showSettings}
|
showSettings={showSettings}
|
||||||
showExplore={showExplore}
|
showExplore={showExplore}
|
||||||
showMe={showMe}
|
showMe={showMe}
|
||||||
|
showProfile={showProfile}
|
||||||
bookmarks={bookmarks}
|
bookmarks={bookmarks}
|
||||||
bookmarksLoading={bookmarksLoading}
|
bookmarksLoading={bookmarksLoading}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
@@ -272,6 +291,9 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|||||||
me={showMe ? (
|
me={showMe ? (
|
||||||
relayPool ? <Me relayPool={relayPool} activeTab={meTab} /> : null
|
relayPool ? <Me relayPool={relayPool} activeTab={meTab} /> : null
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
profile={showProfile && profilePubkey ? (
|
||||||
|
relayPool ? <Me relayPool={relayPool} activeTab={profileTab} pubkey={profilePubkey} /> : null
|
||||||
|
) : undefined}
|
||||||
toastMessage={toastMessage ?? undefined}
|
toastMessage={toastMessage ?? undefined}
|
||||||
toastType={toastType}
|
toastType={toastType}
|
||||||
onClearToast={clearToast}
|
onClearToast={clearToast}
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ import PullToRefreshIndicator from './PullToRefreshIndicator'
|
|||||||
interface MeProps {
|
interface MeProps {
|
||||||
relayPool: RelayPool
|
relayPool: RelayPool
|
||||||
activeTab?: TabType
|
activeTab?: TabType
|
||||||
|
pubkey?: string // Optional pubkey for viewing other users' profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings'
|
type TabType = 'highlights' | 'reading-list' | 'archive' | 'writings'
|
||||||
|
|
||||||
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: propPubkey }) => {
|
||||||
const activeAccount = Hooks.useActiveAccount()
|
const activeAccount = Hooks.useActiveAccount()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
|
const [activeTab, setActiveTab] = useState<TabType>(propActiveTab || 'highlights')
|
||||||
|
|
||||||
|
// Use provided pubkey or fall back to active account
|
||||||
|
const viewingPubkey = propPubkey || activeAccount?.pubkey
|
||||||
|
const isOwnProfile = !propPubkey || (activeAccount?.pubkey === propPubkey)
|
||||||
const [highlights, setHighlights] = useState<Highlight[]>([])
|
const [highlights, setHighlights] = useState<Highlight[]>([])
|
||||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||||
const [readArticles, setReadArticles] = useState<BlogPostPreview[]>([])
|
const [readArticles, setReadArticles] = useState<BlogPostPreview[]>([])
|
||||||
@@ -54,8 +59,8 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
if (!activeAccount) {
|
if (!viewingPubkey) {
|
||||||
setError('Please log in to view your data')
|
setError(isOwnProfile ? 'Please log in to view your data' : 'Invalid profile')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -64,39 +69,48 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
// Seed from cache if available to avoid empty flash
|
// Seed from cache if available to avoid empty flash (own profile only)
|
||||||
const cached = getCachedMeData(activeAccount.pubkey)
|
if (isOwnProfile) {
|
||||||
if (cached) {
|
const cached = getCachedMeData(viewingPubkey)
|
||||||
setHighlights(cached.highlights)
|
if (cached) {
|
||||||
setBookmarks(cached.bookmarks)
|
setHighlights(cached.highlights)
|
||||||
setReadArticles(cached.readArticles)
|
setBookmarks(cached.bookmarks)
|
||||||
|
setReadArticles(cached.readArticles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch highlights, read articles, and writings
|
// Fetch highlights and writings (public data)
|
||||||
const [userHighlights, userReadArticles, userWritings] = await Promise.all([
|
const [userHighlights, userWritings] = await Promise.all([
|
||||||
fetchHighlights(relayPool, activeAccount.pubkey),
|
fetchHighlights(relayPool, viewingPubkey),
|
||||||
fetchReadArticlesWithData(relayPool, activeAccount.pubkey),
|
fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS)
|
||||||
fetchBlogPostsFromAuthors(relayPool, [activeAccount.pubkey], RELAYS)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
setHighlights(userHighlights)
|
setHighlights(userHighlights)
|
||||||
setReadArticles(userReadArticles)
|
|
||||||
setWritings(userWritings)
|
setWritings(userWritings)
|
||||||
|
|
||||||
// Fetch bookmarks using callback pattern
|
// Only fetch private data for own profile
|
||||||
let fetchedBookmarks: Bookmark[] = []
|
if (isOwnProfile && activeAccount) {
|
||||||
try {
|
const userReadArticles = await fetchReadArticlesWithData(relayPool, viewingPubkey)
|
||||||
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
setReadArticles(userReadArticles)
|
||||||
fetchedBookmarks = newBookmarks
|
|
||||||
setBookmarks(newBookmarks)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to load bookmarks:', err)
|
|
||||||
setBookmarks([])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cache with all fetched data
|
// Fetch bookmarks using callback pattern
|
||||||
setCachedMeData(activeAccount.pubkey, userHighlights, fetchedBookmarks, userReadArticles)
|
let fetchedBookmarks: Bookmark[] = []
|
||||||
|
try {
|
||||||
|
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
||||||
|
fetchedBookmarks = newBookmarks
|
||||||
|
setBookmarks(newBookmarks)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load bookmarks:', err)
|
||||||
|
setBookmarks([])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache with all fetched data
|
||||||
|
setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReadArticles)
|
||||||
|
} else {
|
||||||
|
setBookmarks([])
|
||||||
|
setReadArticles([])
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load data:', err)
|
console.error('Failed to load data:', err)
|
||||||
setError('Failed to load data. Please try again.')
|
setError('Failed to load data. Please try again.')
|
||||||
@@ -106,7 +120,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadData()
|
loadData()
|
||||||
}, [relayPool, activeAccount, refreshTrigger])
|
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
||||||
|
|
||||||
// Pull-to-refresh
|
// Pull-to-refresh
|
||||||
const pullToRefreshState = usePullToRefresh(meContainerRef, {
|
const pullToRefreshState = usePullToRefresh(meContainerRef, {
|
||||||
@@ -119,9 +133,9 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
const handleHighlightDelete = (highlightId: string) => {
|
const handleHighlightDelete = (highlightId: string) => {
|
||||||
setHighlights(prev => {
|
setHighlights(prev => {
|
||||||
const updated = prev.filter(h => h.id !== highlightId)
|
const updated = prev.filter(h => h.id !== highlightId)
|
||||||
// Update cache when highlight is deleted
|
// Update cache when highlight is deleted (own profile only)
|
||||||
if (activeAccount) {
|
if (isOwnProfile && viewingPubkey) {
|
||||||
updateCachedHighlights(activeAccount.pubkey, updated)
|
updateCachedHighlights(viewingPubkey, updated)
|
||||||
}
|
}
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
@@ -324,7 +338,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
isRefreshing={loading && pullToRefreshState.canRefresh}
|
isRefreshing={loading && pullToRefreshState.canRefresh}
|
||||||
/>
|
/>
|
||||||
<div className="explore-header">
|
<div className="explore-header">
|
||||||
{activeAccount && <AuthorCard authorPubkey={activeAccount.pubkey} />}
|
{viewingPubkey && <AuthorCard authorPubkey={viewingPubkey} clickable={false} />}
|
||||||
|
|
||||||
{loading && hasData && (
|
{loading && hasData && (
|
||||||
<div className="explore-loading" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0' }}>
|
<div className="explore-loading" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0' }}>
|
||||||
@@ -336,34 +350,38 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab }) => {
|
|||||||
<button
|
<button
|
||||||
className={`me-tab ${activeTab === 'highlights' ? 'active' : ''}`}
|
className={`me-tab ${activeTab === 'highlights' ? 'active' : ''}`}
|
||||||
data-tab="highlights"
|
data-tab="highlights"
|
||||||
onClick={() => navigate('/me/highlights')}
|
onClick={() => navigate(isOwnProfile ? '/me/highlights' : `/p/${propPubkey && nip19.npubEncode(propPubkey)}`)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faHighlighter} />
|
<FontAwesomeIcon icon={faHighlighter} />
|
||||||
<span className="tab-label">Highlights</span>
|
<span className="tab-label">Highlights</span>
|
||||||
<span className="tab-count">({highlights.length})</span>
|
<span className="tab-count">({highlights.length})</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
{isOwnProfile && (
|
||||||
className={`me-tab ${activeTab === 'reading-list' ? 'active' : ''}`}
|
<>
|
||||||
data-tab="reading-list"
|
<button
|
||||||
onClick={() => navigate('/me/reading-list')}
|
className={`me-tab ${activeTab === 'reading-list' ? 'active' : ''}`}
|
||||||
>
|
data-tab="reading-list"
|
||||||
<FontAwesomeIcon icon={faBookmark} />
|
onClick={() => navigate('/me/reading-list')}
|
||||||
<span className="tab-label">Reading List</span>
|
>
|
||||||
<span className="tab-count">({allIndividualBookmarks.length})</span>
|
<FontAwesomeIcon icon={faBookmark} />
|
||||||
</button>
|
<span className="tab-label">Reading List</span>
|
||||||
<button
|
<span className="tab-count">({allIndividualBookmarks.length})</span>
|
||||||
className={`me-tab ${activeTab === 'archive' ? 'active' : ''}`}
|
</button>
|
||||||
data-tab="archive"
|
<button
|
||||||
onClick={() => navigate('/me/archive')}
|
className={`me-tab ${activeTab === 'archive' ? 'active' : ''}`}
|
||||||
>
|
data-tab="archive"
|
||||||
<FontAwesomeIcon icon={faBooks} />
|
onClick={() => navigate('/me/archive')}
|
||||||
<span className="tab-label">Archive</span>
|
>
|
||||||
<span className="tab-count">({readArticles.length})</span>
|
<FontAwesomeIcon icon={faBooks} />
|
||||||
</button>
|
<span className="tab-label">Archive</span>
|
||||||
|
<span className="tab-count">({readArticles.length})</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className={`me-tab ${activeTab === 'writings' ? 'active' : ''}`}
|
className={`me-tab ${activeTab === 'writings' ? 'active' : ''}`}
|
||||||
data-tab="writings"
|
data-tab="writings"
|
||||||
onClick={() => navigate('/me/writings')}
|
onClick={() => navigate(isOwnProfile ? '/me/writings' : `/p/${propPubkey && nip19.npubEncode(propPubkey)}/writings`)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPenToSquare} />
|
<FontAwesomeIcon icon={faPenToSquare} />
|
||||||
<span className="tab-label">Writings</span>
|
<span className="tab-label">Writings</span>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface ThreePaneLayoutProps {
|
|||||||
showSettings: boolean
|
showSettings: boolean
|
||||||
showExplore?: boolean
|
showExplore?: boolean
|
||||||
showMe?: boolean
|
showMe?: boolean
|
||||||
|
showProfile?: boolean
|
||||||
|
|
||||||
// Bookmarks pane
|
// Bookmarks pane
|
||||||
bookmarks: Bookmark[]
|
bookmarks: Bookmark[]
|
||||||
@@ -89,6 +90,9 @@ interface ThreePaneLayoutProps {
|
|||||||
|
|
||||||
// Optional Me content
|
// Optional Me content
|
||||||
me?: React.ReactNode
|
me?: React.ReactNode
|
||||||
|
|
||||||
|
// Optional Profile content
|
||||||
|
profile?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||||
@@ -221,8 +225,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile bookmark button - only show when viewing article (not on settings/explore/me) */}
|
{/* 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 && (
|
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && (
|
||||||
<button
|
<button
|
||||||
className={`fixed z-[900] bg-zinc-800/70 border border-zinc-600/40 rounded-lg text-zinc-200 flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
className={`fixed z-[900] bg-zinc-800/70 border border-zinc-600/40 rounded-lg text-zinc-200 flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
||||||
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
||||||
@@ -241,8 +245,8 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile highlights button - only show when viewing article (not on settings/explore/me) */}
|
{/* 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 && (
|
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && !props.showExplore && !props.showMe && !props.showProfile && (
|
||||||
<button
|
<button
|
||||||
className={`fixed z-[900] border border-zinc-600/40 rounded-lg flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
className={`fixed z-[900] border border-zinc-600/40 rounded-lg flex items-center justify-center transition-all duration-300 active:scale-95 backdrop-blur-sm md:hidden ${
|
||||||
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
showMobileButtons ? 'opacity-90 visible' : 'opacity-0 invisible pointer-events-none'
|
||||||
@@ -320,6 +324,11 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
<>
|
<>
|
||||||
{props.me}
|
{props.me}
|
||||||
</>
|
</>
|
||||||
|
) : props.showProfile && props.profile ? (
|
||||||
|
// Render Profile inside the main pane to keep side panels
|
||||||
|
<>
|
||||||
|
{props.profile}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ContentPanel
|
<ContentPanel
|
||||||
loading={props.readerLoading}
|
loading={props.readerLoading}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/* Profile UI fragments */
|
/* Profile UI fragments */
|
||||||
.author-card-container { display: flex; justify-content: center; padding: 2rem 1rem; }
|
.author-card-container { display: flex; justify-content: center; padding: 2rem 1rem; }
|
||||||
.author-card { display: flex; gap: 1rem; padding: 1.5rem; background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; max-width: 600px; width: 100%; }
|
.author-card { display: flex; gap: 1rem; padding: 1.5rem; background: rgb(24 24 27); /* zinc-900 */ border: 1px solid rgb(63 63 70); /* zinc-700 */ border-radius: 12px; max-width: 600px; width: 100%; transition: all 0.2s ease; }
|
||||||
|
.author-card-clickable:hover { border-color: rgb(99 102 241); /* indigo-500 */ background: rgb(30 30 33); /* slightly lighter */ transform: translateY(-1px); }
|
||||||
|
.author-card-clickable:active { transform: translateY(0); }
|
||||||
.author-card-avatar { flex-shrink: 0; width: 60px; height: 60px; border-radius: 50%; overflow: hidden; background: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; color: rgb(113 113 122); /* zinc-500 */ }
|
.author-card-avatar { flex-shrink: 0; width: 60px; height: 60px; border-radius: 50%; overflow: hidden; background: rgb(39 39 42); /* zinc-800 */ display: flex; align-items: center; justify-content: center; color: rgb(113 113 122); /* zinc-500 */ }
|
||||||
.author-card-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
.author-card-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
.author-card-avatar svg { font-size: 2.5rem; }
|
.author-card-avatar svg { font-size: 2.5rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user