diff --git a/common/messages/de.json b/common/messages/de.json index 01646240..a4e56494 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -871,6 +871,7 @@ "profile.comments.update_error": "Aktualisierung der Empfehlungseinstellungen fehlgeschlagen", "profile.connect.direct_messaging_disabled": "{user} hat Direktnachrichten deaktiviert", "profile.connect.how_this_works": "Wie es funktioniert", + "profile.connect.message_name": "{name} schreiben", "profile.connect.interest_indicators_disabled": "{user} hat Interessenssignale deaktiviert", "profile.connect.load_preferences_failed": "Fehler beim Laden der Einstellungen", "profile.connect.mutual": "Gegenseitig", @@ -1215,6 +1216,8 @@ "profile.optional.want_kids": "Ich möchte Kinder haben", "profile.optional.work": "Arbeit", "profile.personality": "Persönlichkeit", + "profile.photos": "Fotos", + "profile.photos.scroll_sideways": "Seitwärts scrollen", "profile.orientation": "Orientierung", "profile.orientation.aceflux": "Aceflux", "profile.orientation.akioromantic": "Akioromantisch", diff --git a/common/messages/fr.json b/common/messages/fr.json index 1a0e9f98..52428752 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -870,6 +870,7 @@ "profile.comments.update_error": "Échec de la mise à jour des paramètres de recommandation", "profile.connect.direct_messaging_disabled": "{user} a désactivé la messagerie directe", "profile.connect.how_this_works": "Comment ça fonctionne", + "profile.connect.message_name": "Écrire à {name}", "profile.connect.interest_indicators_disabled": "{user} a désactivé les indicateurs d'intérêt", "profile.connect.load_preferences_failed": "Échec du chargement des préférences", "profile.connect.mutual": "Mutuel", @@ -1214,6 +1215,8 @@ "profile.optional.want_kids": "Je souhaite avoir des enfants", "profile.optional.work": "Domaine de travail", "profile.personality": "Personnalité", + "profile.photos": "Photos", + "profile.photos.scroll_sideways": "Faire défiler", "profile.orientation": "Orientation", "profile.orientation.aceflux": "Aceflux", "profile.orientation.akioromantic": "Akioromantique", diff --git a/tests/e2e/utils/seed-showcase.ts b/tests/e2e/utils/seed-showcase.ts index d63dd350..cc942dd4 100644 --- a/tests/e2e/utils/seed-showcase.ts +++ b/tests/e2e/utils/seed-showcase.ts @@ -58,7 +58,7 @@ const DEFAULT_AVATAR = '/images/default-avatar.png' * Missing portraits are not fatal: the profile still seeds, it just wears the default avatar. * * `pinned_url` and `photo_urls` are disjoint: the pinned photo must NOT also appear in `photo_urls`. - * `ProfileGallery` renders `buildArray(pinned_url, photo_urls)` without deduping, so a shared entry + * `useProfilePhotos` renders `buildArray(pinned_url, photo_urls)` without deduping, so a shared entry * shows up twice. (The photo editor wraps the same call in `uniq`, which masks it while editing.) */ function resolvePhotos(profile: ShowcaseProfile) { diff --git a/web/components/profile-about.tsx b/web/components/profile-about.tsx index 596dcb89..a98beac5 100644 --- a/web/components/profile-about.tsx +++ b/web/components/profile-about.tsx @@ -30,6 +30,7 @@ import {Col} from 'web/components/layout/col' import {CustomLink} from 'web/components/links' import {UserHandles} from 'web/components/user/user-handles' import {useChoicesContext} from 'web/hooks/use-choices' +import {useMeasurementSystem} from 'web/hooks/use-measurement-system' import {useLocale, useT} from 'web/lib/locale' import {getSeekingConnectionText} from 'web/lib/profile/seeking' import {convertRace} from 'web/lib/util/convert-types' @@ -64,7 +65,9 @@ export default function ProfileAbout(props: { 5. Languages — practical. 6. Identity nuance — gender and orientation only render when the member wrote something of their own about them, so they are elaboration, not a demographic checkbox to scan. - 7. Background, then activity metadata. */} + 7. Background, then height, then activity metadata. Height sits down here rather than in + the hero line: it is a number people filter on, not part of how anyone introduces + themselves, and beside the name it was taking the weight of one. */} {!omitConnectionGoals && } @@ -81,6 +84,7 @@ export default function ProfileAbout(props: { + {!isCurrentUser && ( <> @@ -520,6 +524,21 @@ function Ethnicity(props: {profile: Profile}) { return } +function Height(props: {profile: Profile}) { + const t = useT() + const {profile} = props + const {measurementSystem} = useMeasurementSystem() + + if (profile.height_in_inches == null) return null + + return ( + + ) +} + function Smoker(props: {profile: Profile}) { const t = useT() const {profile} = props diff --git a/web/components/profile-comment-section.tsx b/web/components/profile-comment-section.tsx index 7806b4b7..0d3714bb 100644 --- a/web/components/profile-comment-section.tsx +++ b/web/components/profile-comment-section.tsx @@ -66,11 +66,22 @@ export const ProfileCommentSection = (props: { trackingLocation={'contract page'} /> ) : ( - /* One compact row, not a panel. This is an invitation, and as a 170px dashed box it - outweighed the endorsements it was asking for — and a dashed border reads as a drop - zone, which is the wrong metaphor. The only card left in this region is the - connection-signal one, which actually holds controls. */ + /* One compact row, not a panel. This is an invitation, and as a 170px box it + outweighed the endorsements it was asking for. The only card left in this region + is the connection-signal one, which actually holds controls. */ + {/* Dashed, unfilled, in body ink rather than the accent: nobody arrives here + meaning to write one, so the control should be findable when the thought + occurs and invisible until then. A solid accent pill asked for the click + harder than the endorsements below it asked to be read. */} + {!isCurrentUser && ( + + )}
{isCurrentUser ? t( @@ -79,17 +90,10 @@ export const ProfileCommentSection = (props: { ) : t( 'profile.comments.other_user_hint', - 'If you know them, write something nice that adds to their profile.', + 'Know {name}? A few honest sentences help strangers trust him.', + {name: onUser.name}, )}
- {!isCurrentUser && ( - - )}
)} diff --git a/web/components/profile/profile-gallery.tsx b/web/components/profile/profile-gallery.tsx deleted file mode 100644 index 8d9dd3f3..00000000 --- a/web/components/profile/profile-gallery.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import clsx from 'clsx' -import {Profile} from 'common/profiles/profile' -import {buildArray} from 'common/util/array' -import Image from 'next/image' -import {useState} from 'react' -import {Col} from 'web/components/layout/col' -import {MediaModal} from 'web/components/media-modal' -import {SignUpButton} from 'web/components/nav/sidebar' -import {useUser} from 'web/hooks/use-user' -import {isVideo} from 'web/lib/firebase/storage' -import {useT} from 'web/lib/locale' - -/** - * Square photo gallery for the profile hero: one main tile plus a thumbnail strip. - * - * Photos are the second thing a visitor checks, so they sit at the top — but the tile is capped at a - * modest size on purpose. A full-bleed hero image would push the bio, which ranks right behind them, - * off the first screen. - */ -const MAIN_SIZE = 300 -const THUMB_SIZE = 64 - -export default function ProfileGallery(props: {profile: Profile; className?: string}) { - const {profile, className} = props - const t = useT() - const currentUser = useUser() - - const urls = buildArray(profile.pinned_url, profile.photo_urls) - const [selected, setSelected] = useState(0) - const [lightboxOpen, setLightboxOpen] = useState(false) - - if (urls.length === 0) return null - - // Signed-out visitors on a members-only profile see the pinned photo and a locked count. - const isLocked = !currentUser && profile.visibility !== 'public' - const visibleUrls = isLocked ? urls.slice(0, 1) : urls - - const selectedUrl = visibleUrls[Math.min(selected, visibleUrls.length - 1)] - - // Every photo gets a thumbnail — they wrap onto as many rows as it takes. The only "+N" left is the - // locked one, where the count is all a signed-out visitor is allowed to know. - const lockedCount = isLocked ? urls.length - 1 : 0 - - return ( - -
- !isLocked && setLightboxOpen(true)} - className={isLocked ? 'cursor-default' : 'cursor-pointer'} - /> -
- - {(visibleUrls.length > 1 || lockedCount > 0) && ( -
- {!isLocked && - visibleUrls.map((url, i) => ( - - ))} - {lockedCount > 0 && ( -
- -
- )} -
- )} - - {/* The whole set goes to the modal, not just the tile that was clicked, so it can page through - them and show each one's description. Descriptions live only there — under the grid they - changed height as you moved between thumbnails and shoved the hero around. */} - } - open={lightboxOpen} - setOpen={setLightboxOpen} - /> - - ) -} - -function MediaTile(props: { - url: string - priority?: boolean - onClick?: () => void - className?: string -}) { - const {url, priority, onClick, className} = props - - if (isVideo(url)) { - return ( -