From 9745237302e67046061e5b6ea7cfa3ee25fdde1d Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Wed, 29 Jul 2026 19:17:12 +0200 Subject: [PATCH] Extract Section and SectionHeading components: refactor to reduce duplication, improve readability, and streamline profile section composition. --- web/components/profile/connect-actions.tsx | 84 +++++++++++----------- web/components/profile/profile-info.tsx | 36 ++-------- web/components/profile/section.tsx | 37 ++++++++++ 3 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 web/components/profile/section.tsx diff --git a/web/components/profile/connect-actions.tsx b/web/components/profile/connect-actions.tsx index 9d8e9e5b..946394c7 100644 --- a/web/components/profile/connect-actions.tsx +++ b/web/components/profile/connect-actions.tsx @@ -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 ( - - {profile.allow_direct_messaging || matches.length > 0 ? ( - - ) : ( -

- {t('profile.connect.direct_messaging_disabled', '{user} turned off direct messaging', { - user: user.name, - })} -

- )} + <> +
+ {profile.allow_direct_messaging || matches.length > 0 ? ( + + ) : ( +

+ {t('profile.connect.direct_messaging_disabled', '{user} turned off direct messaging', { + user: user.name, + })} +

+ )} +
- {/* 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. */} -
- -

- {t('profile.connect.private_connection_signal', 'Signal your interest — privately')} -

- {/* Explaining the mechanism is only worth the row when the mechanism is available. */} - {/*{hasSignalControls && (*/} - {/* setShowHelp(!showHelp)}*/} - {/* aria-expanded={showHelp}*/} - {/* >*/} - {/* */} - {/* {t('profile.connect.how_this_works', 'How this works')}*/} - {/* */} - {/*)}*/} -
+
+ {/* Explaining the mechanism is only worth the row when the mechanism is available. */} + {/*{hasSignalControls && (*/} + {/* setShowHelp(!showHelp)}*/} + {/* aria-expanded={showHelp}*/} + {/* >*/} + {/* */} + {/* {t('profile.connect.how_this_works', 'How this works')}*/} + {/* */} + {/*)}*/} {profile.allow_interest_indicating ? ( <> {hasSignalControls && ( -

+

{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 ? ( -

+

{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}) { )} ) : ( -

+

{t('profile.turned_off_interest_indicators', '{user} turned off interest indicators', { user: user.name, })}

)} -
- + + ) } diff --git a/web/components/profile/profile-info.tsx b/web/components/profile/profile-info.tsx index ddc0f389..ad7f9d1d 100644 --- a/web/components/profile/profile-info.tsx +++ b/web/components/profile/profile-info.tsx @@ -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 ( -
- {title != null && {title}} - {children} -
- ) -} - /** * 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 ( -

- {children} -

- ) -} - function ProfileContent(props: { user: User userActivity?: UserActivity @@ -557,11 +531,9 @@ function ProfileContent(props: { /> - {!isCurrentUser && ( -
- -
- )} + {/* 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 && } )} {!currentUser && } diff --git a/web/components/profile/section.tsx b/web/components/profile/section.tsx new file mode 100644 index 00000000..4749770c --- /dev/null +++ b/web/components/profile/section.tsx @@ -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 ( +
+ {title != null && {title}} + {children} +
+ ) +} + +export function SectionHeading(props: {children: ReactNode; className?: string}) { + const {children, className} = props + return ( +

+ {children} +

+ ) +}