Extract Section and SectionHeading components: refactor to reduce duplication, improve readability, and streamline profile section composition.

This commit is contained in:
MartinBraquet
2026-07-29 19:17:12 +02:00
parent 63333bfc0f
commit 9745237302
3 changed files with 82 additions and 75 deletions

View File

@@ -5,9 +5,9 @@ import {CheckIcon, SparklesIcon} from 'lucide-react'
import {useEffect, useState} from 'react'
import toast from 'react-hot-toast'
import ReactMarkdown from 'react-markdown'
import {Col} from 'web/components/layout/col'
import {Row} from 'web/components/layout/row'
import {SendMessageButton} from 'web/components/messaging/send-message-button'
import {Section} from 'web/components/profile/section'
import {Tooltip} from 'web/components/widgets/tooltip'
import {shortenName} from 'web/components/widgets/user-link'
import {useProfile} from 'web/hooks/use-profile'
@@ -95,52 +95,50 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
const hasSignalControls = !!profile.allow_interest_indicating && !noSharedTypes
return (
<Col className="w-full gap-8">
{profile.allow_direct_messaging || matches.length > 0 ? (
<SendMessageButton
toUser={user}
profile={profile}
text={t('profile.connect.message_name', 'Message {name}', {
name: shortenName(user.name),
})}
/>
) : (
<p className="text-ink-500 text-sm">
{t('profile.connect.direct_messaging_disabled', '{user} turned off direct messaging', {
user: user.name,
})}
</p>
)}
<>
<Section id="connect" title={t('profile.connect.title', 'Connect')}>
{profile.allow_direct_messaging || matches.length > 0 ? (
<SendMessageButton
toUser={user}
profile={profile}
text={t('profile.connect.message_name', 'Message {name}', {
name: shortenName(user.name),
})}
/>
) : (
<p className="text-ink-500 text-sm">
{t('profile.connect.direct_messaging_disabled', '{user} turned off direct messaging', {
user: user.name,
})}
</p>
)}
</Section>
{/* No surface at all. The chips are pills, and a pill is already this page's signal for "you can
{/* Its own section rather than a subheading under Connect: it is a distinct thing to do, not a
detail of messaging, and a smaller heading nested inside another read as one.
No surface at all. The chips are pills, and a pill is already this page's signal for "you can
click this" — the card was restating it in a heavier way, and it made this block outweigh the
two actions above it, which are boxless despite being just as actionable. Nothing on this page
is a card now; affordance is carried by the controls themselves. */}
<div>
<Row className="items-baseline justify-between gap-3">
<h3
className="text-ink-400 font-dm-sans uppercase"
style={{fontSize: '10px', letterSpacing: '0.18em'}}
>
{t('profile.connect.private_connection_signal', 'Signal your interest — privately')}
</h3>
{/* Explaining the mechanism is only worth the row when the mechanism is available. */}
{/*{hasSignalControls && (*/}
{/* <button*/}
{/* className="text-ink-500 hover:text-primary-600 flex flex-none items-center gap-1 text-xs transition-colors"*/}
{/* onClick={() => setShowHelp(!showHelp)}*/}
{/* aria-expanded={showHelp}*/}
{/* >*/}
{/* <InfoIcon className="h-3.5 w-3.5" />*/}
{/* {t('profile.connect.how_this_works', 'How this works')}*/}
{/* </button>*/}
{/*)}*/}
</Row>
<Section
title={t('profile.connect.private_connection_signal', 'Signal your interest — privately')}
>
{/* Explaining the mechanism is only worth the row when the mechanism is available. */}
{/*{hasSignalControls && (*/}
{/* <button*/}
{/* className="text-ink-500 hover:text-primary-600 flex flex-none items-center gap-1 text-xs transition-colors"*/}
{/* onClick={() => setShowHelp(!showHelp)}*/}
{/* aria-expanded={showHelp}*/}
{/* >*/}
{/* <InfoIcon className="h-3.5 w-3.5" />*/}
{/* {t('profile.connect.how_this_works', 'How this works')}*/}
{/* </button>*/}
{/*)}*/}
{profile.allow_interest_indicating ? (
<>
{hasSignalControls && (
<p className="text-ink-500 mt-2 text-sm">
<p className="text-ink-500 text-sm">
{t(
'profile.wont_be_notified_unless_mutual',
"Pick what you'd be open to with {name}. They're never notified unless they pick the same thing — then you both hear about it at once.",
@@ -158,7 +156,7 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
{/* Every option locked means the chips can only say "no" three times over, in a shape that
still looks pressable. Say it once instead, and give back the height. */}
{noSharedTypes ? (
<p className="text-ink-500 mt-3 text-sm">
<p className="text-ink-500 text-sm">
{t(
'profile.connect.no_shared_types',
"You and {name} aren't open to the same kind of connection.",
@@ -239,13 +237,13 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
)}
</>
) : (
<p className="text-ink-500 mt-2 text-sm">
<p className="text-ink-500 text-sm">
{t('profile.turned_off_interest_indicators', '{user} turned off interest indicators', {
user: user.name,
})}
</p>
)}
</div>
</Col>
</Section>
</>
)
}

View File

@@ -13,6 +13,7 @@ import {ProfileHeaderActions} from 'web/components/profile/profile-header'
import ProfileHero from 'web/components/profile/profile-hero'
import ProfilePhotoCarousel from 'web/components/profile/profile-photo-carousel'
import {useProfilePhotos} from 'web/components/profile/profile-photos'
import {Section, SectionHeading} from 'web/components/profile/section'
import ProfileAbout, {
hasAccessibility,
hasBigFive,
@@ -376,21 +377,6 @@ function RailBlocks(props: {
)
}
/**
* A section of the main reading column. No border and no card — the column is prose, and whitespace
* plus a small-caps label separates sections better than five stacked boxes do.
*/
function Section(props: {title?: ReactNode; id?: string; children: ReactNode; testId?: string}) {
const {title, id, children, testId} = props
if (children == null) return null
return (
<section id={id} className="min-w-0 scroll-mt-24" data-testid={testId}>
{title != null && <SectionHeading>{title}</SectionHeading>}
{children}
</section>
)
}
/**
* A block of the attribute rail: label, rule, content — the same object as `Section` on the left, with
* a rule added because the rail's rows are also rule-separated and the heading needs to sit outside
@@ -419,18 +405,6 @@ function RailSection(props: {title?: ReactNode; children: ReactNode}) {
)
}
function SectionHeading(props: {children: ReactNode; className?: string}) {
const {children, className} = props
return (
<h2
className={clsx('text-ink-400 font-dm-sans mb-5 font-normal uppercase', className)}
style={{fontSize: '10px', letterSpacing: '0.18em'}}
>
{children}
</h2>
)
}
function ProfileContent(props: {
user: User
userActivity?: UserActivity
@@ -557,11 +531,9 @@ function ProfileContent(props: {
/>
</Section>
{!isCurrentUser && (
<Section id="connect" title={t('profile.connect.title', 'Connect')}>
<ConnectActions user={user} profile={profile} />
</Section>
)}
{/* Renders its own sections — reaching out and signalling interest privately are two
separate things to do, and each gets the same heading as everything else here. */}
{!isCurrentUser && <ConnectActions user={user} profile={profile} />}
</Col>
)}
{!currentUser && <SignUpButton text={t('profile.info.signup_to_see', signupMessage)} />}

View File

@@ -0,0 +1,37 @@
import clsx from 'clsx'
import {ReactNode} from 'react'
/**
* A section of the main reading column. No border and no card — the column is prose, and whitespace
* plus a small-caps label separates sections better than five stacked boxes do.
*
* Lives here rather than in `profile-info` so the sections it composes — Connect and the interest
* signal among them — can build themselves out of the same heading without importing their parent.
*/
export function Section(props: {
title?: ReactNode
id?: string
children: ReactNode
testId?: string
}) {
const {title, id, children, testId} = props
if (children == null) return null
return (
<section id={id} className="min-w-0 scroll-mt-24" data-testid={testId}>
{title != null && <SectionHeading>{title}</SectionHeading>}
{children}
</section>
)
}
export function SectionHeading(props: {children: ReactNode; className?: string}) {
const {children, className} = props
return (
<h2
className={clsx('text-ink-400 font-dm-sans mb-5 font-normal uppercase', className)}
style={{fontSize: '10px', letterSpacing: '0.18em'}}
>
{children}
</h2>
)
}