Major re-design of the profile pages

This commit is contained in:
MartinBraquet
2026-05-08 13:55:50 +02:00
parent 09ca5778b7
commit ce10ddf366
47 changed files with 1792 additions and 703 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.compassconnections.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 101
versionName "1.22.0"
versionCode 102
versionName "1.23.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -58,3 +58,9 @@ export function getLocationText(
return `${city}${stateOrCountry && ', '}${stateOrCountry}`
}
export function getGoogleMapsUrl(locationText: string) {
let text = locationText.split(', ').join('+')
text = text.replace(' ', '+')
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(text)}`
}

View File

@@ -29,7 +29,7 @@ export class SettingsPage {
this.themeToggle = page.getByTestId('settings-dark-light-toggle')
this.fontPicker = page.getByTestId('settings-font-picker')
this.downloadProfileJSONDataButton = page.getByRole('button', {
name: 'Download all my data (JSON)',
name: 'Download all my data',
})
this.manageHiddenProfilesButton = page.getByRole('button', {name: 'Manage hidden profiles'})
this.hiddenProfilesSection = page.getByTestId('hidden-profiles')

View File

@@ -42,7 +42,7 @@ export function AnswerCompatibilityQuestionButton(props: {
onClick={() => setOpen(true)}
color="none"
className={
'px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 hover:text-ink-900'
'px-3 py-2 rounded-md border border-primary-600 text-primary-800 hover:bg-primary-50'
}
>
{t('answers.answer.cta', 'Answer{core} Questions', {
@@ -78,7 +78,7 @@ export function CompatibilityPageButton() {
return (
<Link
href="/compatibility"
className="px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 flex items-center justify-center text-center"
className="px-3 py-2 rounded-md border border-primary-600 text-primary-800 hover:bg-primary-50 flex items-center justify-center text-center text-sm"
>
{t('answers.answer.view_list', 'View List of Questions')}
</Link>

View File

@@ -26,7 +26,6 @@ import {
import {Col} from 'web/components/layout/col'
import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal'
import {Row} from 'web/components/layout/row'
import {CompatibleBadge} from 'web/components/widgets/compatible-badge'
import {Input} from 'web/components/widgets/input'
import {Linkify} from 'web/components/widgets/linkify'
import {Pagination} from 'web/components/widgets/pagination'
@@ -45,6 +44,7 @@ import {useUser} from 'web/hooks/use-user'
import {useT} from 'web/lib/locale'
import {db} from 'web/lib/supabase/db'
import {CompatibleBadge} from '../widgets/compatible-badge'
import {Subtitle} from '../widgets/profile-subtitle'
import {
AnswerCompatibilityQuestionButton,
@@ -57,13 +57,12 @@ import {
deleteCompatibilityAnswer,
getEmptyAnswer,
IMPORTANCE_CHOICES,
IMPORTANCE_DISPLAY_COLORS,
submitCompatibilityAnswer,
} from './answer-compatibility-question-content'
import {PreferredList, PreferredListNoComparison} from './compatibility-question-preferred-list'
import {PinQuestionButton} from './pin-question-button'
const NUM_QUESTIONS_TO_SHOW = 4
const NUM_QUESTIONS_TO_SHOW = 8
const NUM_PINNED_QUESTIONS_TO_SHOW = 4
export function separateQuestionsArray(
@@ -213,18 +212,6 @@ export function CompatibilityQuestionsDisplay(props: {
return (
<Col className="gap-4">
<Row className={'gap-8'}>
<Subtitle>
{isCurrentUser
? t('answers.display.your_prompts', 'Compatibility Prompts')
: t('answers.display.user_prompts', 'Compatibility Prompts', {
name: shortenName(user.name),
})}
</Subtitle>
{compatibilityScore && (
<CompatibleBadge compatibility={compatibilityScore} className={'mt-7 mr-4'} />
)}
</Row>
{pinnedAnswers.length > 0 && (
<Col className="gap-3">
<PinIcon />
@@ -254,7 +241,7 @@ export function CompatibilityQuestionsDisplay(props: {
)}
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
{answeredQuestions.length > 0 && (
<div className="relative mt-3 w-full max-w-[50%] xl:max-w-[600px]">
<div className="relative mt-3 w-full max-w-[50%] xl:max-w-[400px]">
{/*<input*/}
{/* type="text"*/}
{/* placeholder={t('answers.search_placeholder', 'Search prompts...')}*/}
@@ -283,6 +270,9 @@ export function CompatibilityQuestionsDisplay(props: {
user={user}
profile={profile}
/>
{compatibilityScore && (
<CompatibleBadge compatibility={compatibilityScore} className={'mt-5 mr-4'} />
)}
</Row>
{answeredQuestions.length <= 0 ? (
<span className="text-ink-600 text-sm">
@@ -391,6 +381,7 @@ export function CompatibilityAnswerBlock(props: {
refreshCompatibilityAll: () => void
fromProfilePage?: Profile
showCommunityInfo?: boolean
className?: string
}) {
const {
answer,
@@ -400,6 +391,7 @@ export function CompatibilityAnswerBlock(props: {
isCurrentUser,
refreshCompatibilityAll,
fromProfilePage,
className,
} = props
const showCommunityInfo = props.showCommunityInfo === undefined ? true : props.showCommunityInfo
@@ -452,12 +444,13 @@ export function CompatibilityAnswerBlock(props: {
return (
<Col
data-testid="profile-compatibility-section"
className={
'bg-canvas-50 border border-canvas-200 flex-grow gap-2 whitespace-pre-line rounded-xl p-4 leading-relaxed'
}
className={clsx(
'bg-canvas-200/20 border border-canvas-200 flex-grow gap-2 whitespace-pre-line rounded-xl p-4 leading-relaxed',
className,
)}
>
<Row
className="justify-between gap-1 font-semibold"
className="justify-between gap-1 font-medium"
data-testid="profile-compatibility-question"
>
{question.question}
@@ -535,18 +528,21 @@ export function CompatibilityAnswerBlock(props: {
</Row>
{answerText && (
<Row
className="bg-canvas-200 w-fit gap-1 rounded-xl px-2 py-1 text-sm"
className="border border-primary-200 bg-primary-50 text-primary-700 w-fit gap-1 rounded-full px-3 py-1 text-sm"
data-testid="profile-compatibility-question-answer"
>
{answerText}
</Row>
)}
<Row className="px-2" data-testid="profile-compatibility-question-answer-explanation">
<Row
className="px-2 text-sm text-ink-500 leading-relaxed pl-2 border-l-2 border-canvas-300 ml-1"
data-testid="profile-compatibility-question-answer-explanation"
>
{answer?.explanation && <Linkify className="" text={`"${answer.explanation}"`} />}
</Row>
{distinctPreferredAnswersText.length > 0 && (
<Col className="gap-2">
<div className="text-sm">
<div className="text-xs font-semibold uppercase tracking-wider text-ink-300">
{preferredDoesNotIncludeAnswerText
? t('answers.display.acceptable', 'Acceptable')
: t('answers.display.also_acceptable', 'Also acceptable')}
@@ -556,7 +552,10 @@ export function CompatibilityAnswerBlock(props: {
data-testid="profile-compatibility-question-acceptable-answer"
>
{distinctPreferredAnswersText.map((text) => (
<Row key={text} className="bg-canvas-200 w-fit gap-1 rounded-xl px-2 py-1 text-sm">
<Row
key={text}
className="border border-canvas-200 bg-canvas-100 text-ink-500 w-fit gap-1 rounded-full px-3 py-1 text-sm"
>
{text}
</Row>
))}
@@ -716,10 +715,10 @@ function CompatibilityDisplay(props: {
<button
onClick={() => setOpen(true)}
className={clsx(
'text-ink-1000 h-fit w-28 rounded-full px-2 py-0.5 text-xs transition-colors',
'border text-ink-1000 h-fit w-28 rounded-full px-2 py-0.5 text-xs transition-colors',
answerCompatibility
? 'bg-green-500/20 hover:bg-green-500/30'
: 'bg-red-500/20 hover:bg-red-500/30',
? 'bg-green-500/10 text-green-800 border-green-500/25 hover:bg-green-500/30'
: 'bg-red-500/20 text-red-800 border-red-500/25 hover:bg-red-500/30',
)}
>
{answerCompatibility
@@ -803,16 +802,51 @@ function ImportanceDisplay(props: {importance: number}) {
function ImportanceButton(props: {importance: number; onClick: () => void; className?: string}) {
const {importance, onClick, className} = props
// Color scheme based on importance level
const importanceColors = {
3: {
// Very Important — full primary amber
background: 'rgb(var(--color-primary-50))',
color: 'rgb(var(--color-primary-700))',
border: 'rgb(var(--color-primary-200))',
},
2: {
// Important — softer amber, slightly stepped back
background: 'rgb(var(--color-primary-50))',
color: 'rgb(var(--color-primary-600))',
border: 'rgb(var(--color-primary-100))',
},
1: {
// Somewhat Important — warm neutral
background: 'rgb(var(--color-canvas-100))',
color: 'rgb(var(--color-ink-500))',
border: 'rgb(var(--color-canvas-300))',
},
0: {
// Not Important — near-invisible
background: 'rgb(var(--color-canvas-50))',
color: 'rgb(var(--color-ink-300))',
border: 'rgb(var(--color-canvas-200))',
},
}
const colors =
importanceColors[importance as keyof typeof importanceColors] || importanceColors[3]
return (
<button
onClick={onClick}
className={clsx(
'text-ink-1000 h-fit rounded-full px-2 py-0.5 text-xs transition-colors',
// Longer width for "Somewhat important"
'h-fit rounded-full px-2 py-0.5 text-xs font-medium transition-colors',
importance === 1 ? 'w-36' : 'w-28',
IMPORTANCE_DISPLAY_COLORS[importance],
className,
)}
style={{
background: colors.background,
color: colors.color,
border: `1px solid ${colors.border}`,
}}
>
<ImportanceDisplay importance={importance} />
</button>

View File

@@ -17,7 +17,7 @@ export function BackButton(props: {className?: string}) {
<button
type="button"
className={clsx(
'text-ink-500 hover:text-ink-900 inline-flex items-center gap-2 text-sm',
'text-ink-500 hover:text-primary-700 inline-flex items-center gap-2 text-sm transition-all',
className,
)}
>

View File

@@ -27,7 +27,7 @@ export function BioBlock(props: {
<Col
className={clsx(
'flex-grow whitespace-pre-line rounded-md leading-relaxed',
!edit && 'px-3 py-2',
!edit && 'px-0 py-2',
)}
>
<Row className="w-full">
@@ -51,6 +51,7 @@ export function BioBlock(props: {
text={t('more_options_user.edit_bio', 'Bio options')}
noTap
testId="profile-bio-options"
className={'h-fit'}
>
<DropdownMenu
items={[

View File

@@ -8,7 +8,6 @@ import {useTextEditor} from 'web/components/widgets/editor'
import {Tooltip} from 'web/components/widgets/tooltip'
import {useT} from 'web/lib/locale'
import {Subtitle} from '../widgets/profile-subtitle'
import {BioBlock} from './profile-bio-block'
export default function TooShortBio() {
@@ -39,7 +38,6 @@ export function ProfileBio(props: {
const [edit, setEdit] = useState(false)
const editor = useTextEditor({defaultValue: ''})
const [textLength, setTextLength] = useState(MAX_INT)
const t = useT()
useEffect(() => {
if (!editor) return
@@ -53,7 +51,6 @@ export function ProfileBio(props: {
return (
<Col>
{textLength < MIN_BIO_LENGTH && !edit && isCurrentUser && <TooShortBio />}
<Subtitle className="mb-4">{t('profile.bio.about_me', 'About Me')}</Subtitle>
<BioBlock
isCurrentUser={isCurrentUser}
profile={profile}

View File

@@ -21,6 +21,7 @@ export type ColorType =
| 'yellow-outline'
| 'gold'
| 'none'
| 'primary'
const sizeClasses = {
'2xs': 'px-2 py-1 text-xs',
@@ -65,6 +66,8 @@ export function buttonClass(size: SizeType, color: ColorType) {
gradient,
'enabled:!bg-gradient-to-br from-yellow-400 via-yellow-100 to-yellow-300 dark:from-yellow-600 dark:via-yellow-200 dark:to-yellow-400 !text-gray-900',
],
color === 'primary' &&
'w-fit border-canvas-300 text-primary-700 bg-canvas-200 hover:bg-canvas-300 mb-4 !rounded-full border px-4 py-2 text-sm transition-colors',
)
}

View File

@@ -41,7 +41,7 @@ export function CopyLinkOrShareButton(props: {
return (
<ToolTipOrDiv
hasChildren={!!children}
text={tooltip ?? t('copy_link_button.copy_link', 'Copy link')}
text={tooltip ?? t('copy_link_button.copy_link', 'Copy Link')}
noTap
placement="bottom"
>
@@ -49,7 +49,7 @@ export function CopyLinkOrShareButton(props: {
onClick={onClick}
className={clsx(
className,
'text-primary-700 hover:text-primary-800 gap-2',
'text-ink-500 hover:text-primary-800 gap-2',
isSuccess && 'duration-[25ms]',
)}
disabled={!url}
@@ -59,13 +59,13 @@ export function CopyLinkOrShareButton(props: {
{isSuccess ? (
<CheckIcon
strokeWidth={'3'}
className={clsx(iconClassName ?? 'h-[1.1rem]')}
className={clsx(iconClassName, 'h-[1.1rem]')}
aria-hidden="true"
/>
) : (
<LinkIcon
strokeWidth={'2.5'}
className={clsx(iconClassName ?? 'h-[1.1rem]')}
className={clsx(iconClassName, 'h-[1.1rem]')}
aria-hidden="true"
/>
)}
@@ -157,7 +157,7 @@ export function SimpleCopyTextButton(props: {
return (
<IconButton onClick={onClick} className={className} disabled={!text}>
<Tooltip
text={tooltip ?? t('copy_link_button.copy_link', 'Copy link')}
text={tooltip ?? t('copy_link_button.copy_link', 'Copy Link')}
noTap
placement="bottom"
>

View File

@@ -44,7 +44,7 @@ export function MoreOptionsUserButton(props: {user: User}) {
<Tooltip text={t('more_options_user.more_options', 'More Options')} noTap>
<Button
color={'gray-white'}
className="rounded-none px-6"
className="border-canvas-300 flex items-center gap-1.5 rounded-lg border px-[8px] py-2 text-sm text-primary-700 transition-colors hover:border-primary-400 hover:bg-primary-50"
onClick={() => setIsModalOpen(true)}
>
<EllipsisHorizontalIcon className={clsx('h-5 w-5 flex-shrink-0')} aria-hidden="true" />

View File

@@ -34,7 +34,7 @@ export function EmailVerificationButton() {
color={'gray-outline'}
onClick={() => sendVerificationEmail(firebaseUser, t)}
disabled={isEmailVerified}
className={'w-fit'}
className={'w-fit !text-ink-500'}
>
{isEmailVerified
? t('settings.email.verified', 'Email Verified ✔️')

View File

@@ -267,9 +267,9 @@ function Filters(props: {
selection={
<RelationshipFilterText
relationship={filters.pref_relation_styles as RelationshipType[]}
highlightedClass={
hasAny(filters.pref_relation_styles) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.pref_relation_styles) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -287,7 +287,7 @@ function Filters(props: {
location={locationFilterProps.location}
radius={locationFilterProps.radius}
youProfile={youProfile}
highlightedClass={!locationFilterProps.location ? 'text-ink-900' : 'text-primary-600'}
// highlightedClass={!locationFilterProps.location ? 'text-ink-900' : 'text-primary-600'}
/>
}
>
@@ -305,7 +305,7 @@ function Filters(props: {
<AgeFilterText
pref_age_min={filters.pref_age_min}
pref_age_max={filters.pref_age_max}
highlightedClass={noMinAge && noMaxAge ? 'text-ink-900' : 'text-primary-600'}
// highlightedClass={noMinAge && noMaxAge ? 'text-ink-900' : 'text-primary-600'}
/>
}
>
@@ -321,7 +321,7 @@ function Filters(props: {
selection={
<GenderFilterText
gender={filters.genders as Gender[]}
highlightedClass={hasAny(filters.genders) ? 'text-primary-600' : 'text-ink-900'}
// highlightedClass={hasAny(filters.genders) ? 'text-primary-600' : 'text-ink-900'}
/>
}
>
@@ -348,11 +348,11 @@ function Filters(props: {
<RelationshipStatusFilterText
options={filters.relationship_status as string[]}
defaultLabel={t('filter.relationship_status.any', 'Any')}
highlightedClass={
hasAny(filters.relationship_status || undefined)
? 'text-primary-600'
: 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.relationship_status || undefined)
// ? 'text-primary-600'
// : 'text-ink-900'
// }
/>
}
>
@@ -368,11 +368,11 @@ function Filters(props: {
selection={
<RomanticFilterText
relationship={filters.pref_romantic_styles as RomanticType[]}
highlightedClass={
hasAny(filters.pref_romantic_styles || undefined)
? 'text-primary-600'
: 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.pref_romantic_styles || undefined)
// ? 'text-primary-600'
// : 'text-ink-900'
// }
/>
}
>
@@ -388,11 +388,11 @@ function Filters(props: {
selection={
<HasKidsLabel
has_kids={filters.has_kids ?? -1}
highlightedClass={
filters.has_kids != null && filters.has_kids !== -1
? 'text-primary-600'
: 'text-ink-900'
}
// highlightedClass={
// filters.has_kids != null && filters.has_kids !== -1
// ? 'text-primary-600'
// : 'text-ink-900'
// }
/>
}
>
@@ -408,11 +408,11 @@ function Filters(props: {
selection={
<KidsLabel
strength={filters.wants_kids_strength ?? -1}
highlightedClass={
filters.wants_kids_strength != null && filters.wants_kids_strength !== -1
? 'text-primary-600'
: 'text-ink-900'
}
// highlightedClass={
// filters.wants_kids_strength != null && filters.wants_kids_strength !== -1
// ? 'text-primary-600'
// : 'text-ink-900'
// }
/>
}
>
@@ -439,9 +439,9 @@ function Filters(props: {
radius={raisedInLocationFilterProps.radius}
labelPrefix={t('filter.raised_in', 'Grew up')}
youProfile={youProfile}
highlightedClass={
!raisedInLocationFilterProps.location ? 'text-ink-900' : 'text-primary-600'
}
// highlightedClass={
// !raisedInLocationFilterProps.location ? 'text-ink-900' : 'text-primary-600'
// }
/>
}
>
@@ -459,9 +459,9 @@ function Filters(props: {
selection={
<EducationFilterText
options={filters.education_levels as string[]}
highlightedClass={
hasAny(filters.education_levels) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.education_levels) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -477,9 +477,9 @@ function Filters(props: {
<InterestFilterText
options={filters.work as string[] | undefined}
label={'work'}
highlightedClass={
hasAny(filters.work || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.work || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -508,9 +508,9 @@ function Filters(props: {
<InterestFilterText
options={filters.interests as string[] | undefined}
label={'interests'}
highlightedClass={
hasAny(filters.interests || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.interests || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -531,9 +531,9 @@ function Filters(props: {
<InterestFilterText
options={filters.causes as string[] | undefined}
label={'causes'}
highlightedClass={
hasAny(filters.causes || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.causes || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -553,9 +553,9 @@ function Filters(props: {
selection={
<DietFilterText
options={filters.diet as DietType[] | undefined}
highlightedClass={
hasAny(filters.diet || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.diet || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -577,13 +577,13 @@ function Filters(props: {
<DrinksFilterText
drinks_min={filters.drinks_min}
drinks_max={filters.drinks_max}
highlightedClass={(() => {
const [noMinDrinks, noMaxDrinks] = getNoMinMaxDrinks(
filters.drinks_min,
filters.drinks_max,
)
return noMinDrinks && noMaxDrinks ? 'text-ink-900' : 'text-primary-600'
})()}
// highlightedClass={(() => {
// const [noMinDrinks, noMaxDrinks] = getNoMinMaxDrinks(
// filters.drinks_min,
// filters.drinks_max,
// )
// return noMinDrinks && noMaxDrinks ? 'text-ink-900' : 'text-primary-600'
// })()}
/>
}
>
@@ -598,7 +598,7 @@ function Filters(props: {
selection={
<SmokerFilterText
is_smoker={filters.is_smoker}
highlightedClass={filters.is_smoker == null ? 'text-ink-900' : 'text-primary-600'}
// highlightedClass={filters.is_smoker == null ? 'text-ink-900' : 'text-primary-600'}
/>
}
>
@@ -613,9 +613,9 @@ function Filters(props: {
selection={
<PsychedelicsFilterText
options={filters.psychedelics as string[] | undefined}
highlightedClass={
hasAny(filters.psychedelics || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.psychedelics || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -630,9 +630,9 @@ function Filters(props: {
selection={
<CannabisFilterText
options={filters.cannabis as string[] | undefined}
highlightedClass={
hasAny(filters.cannabis || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.cannabis || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -647,9 +647,9 @@ function Filters(props: {
selection={
<LanguageFilterText
options={filters.languages as string[] | undefined}
highlightedClass={
hasAny(filters.languages || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.languages || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -672,9 +672,9 @@ function Filters(props: {
selection={
<PoliticalFilterText
options={filters.political_beliefs as string[] | undefined}
highlightedClass={
hasAny(filters.political_beliefs || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.political_beliefs || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -689,9 +689,9 @@ function Filters(props: {
selection={
<ReligionFilterText
options={filters.religion as string[] | undefined}
highlightedClass={
hasAny(filters.religion || undefined) ? 'text-primary-600' : 'text-ink-900'
}
// highlightedClass={
// hasAny(filters.religion || undefined) ? 'text-primary-600' : 'text-ink-900'
// }
/>
}
>
@@ -715,7 +715,7 @@ function Filters(props: {
<MbtiFilterText
options={filters.mbti as string[] | undefined}
defaultLabel={t('filter.any_mbti', 'Any MBTI')}
highlightedClass={hasAny(filters.mbti) ? 'text-primary-600' : 'text-ink-900'}
// highlightedClass={hasAny(filters.mbti) ? 'text-primary-600' : 'text-ink-900'}
/>
}
>
@@ -730,7 +730,7 @@ function Filters(props: {
selection={
<Big5FilterText
filters={filters}
highlightedClass={hasAnyBig5Filter(filters) ? 'text-primary-600' : 'text-ink-900'}
// highlightedClass={hasAnyBig5Filter(filters) ? 'text-primary-600' : 'text-ink-900'}
/>
}
>
@@ -753,7 +753,7 @@ function Filters(props: {
selection={
<LastActiveFilterText
last_active={filters.last_active}
highlightedClass={!filters.last_active ? 'text-ink-900' : 'text-primary-600'}
// highlightedClass={!filters.last_active ? 'text-ink-900' : 'text-primary-600'}
/>
}
>
@@ -767,7 +767,7 @@ function Filters(props: {
setOpenFilter={setOpenFilter}
isActive={!!filters.hasPhoto}
selection={
<span className={clsx(!filters.hasPhoto ? 'text-ink-900' : 'text-primary-600')}>
<span>
{filters.hasPhoto
? t('filter.has_photo', 'Has photos')
: t('filter.has_photo.photos', 'Photos')}

View File

@@ -1,12 +1,17 @@
import {ReactNode} from 'react'
import {Row} from 'web/components/layout/row'
export function IconWithInfo(props: {text: string; icon: ReactNode}) {
const {text, icon} = props
export function IconWithInfo(props: {text?: string; icon: ReactNode; children?: ReactNode}) {
const {text, icon, children} = props
return (
<Row className="items-start gap-1">
<div className="text-ink-500 mt-1">{icon}</div>
{text}
<Row className="items-center gap-1" style={{gap: '5px'}}>
<div className="mt-0.5" style={{width: '14px', height: '14px'}}>
{icon}
</div>
<span style={{fontSize: '13.5px'}}>
{text}
{children}
</span>
</Row>
)
}

View File

@@ -1,16 +1,28 @@
import Link from 'next/link'
export const CustomLink = ({href, children}: {href?: string; children: React.ReactNode}) => {
export const CustomLink = ({
href,
children,
className,
}: {
href?: string
children: React.ReactNode
className?: string
}) => {
if (!href) return <>{children}</>
// If href is internal, use Next.js Link
if (href.startsWith('/')) {
return <Link href={href}>{children}</Link>
return (
<Link href={href} className={className}>
{children}
</Link>
)
}
// For external links, fall back to <a>
return (
<a href={href} target="_blank" rel="noopener noreferrer">
<a href={href} target="_blank" rel="noopener noreferrer" className={className}>
{children}
</a>
)

View File

@@ -142,7 +142,7 @@ export const SendMessageButton = (props: {
{text ? (
<Button
className={clsx('h-fit gap-1', disabled && 'opacity-50 cursor-not-allowed')}
color={'gray-outline'}
color={'primary'}
onClick={messageButtonClicked}
disabled={disabled}
>
@@ -162,19 +162,17 @@ export const SendMessageButton = (props: {
/>
</button>
) : (
<Button
size={'sm'}
<button
onClick={messageButtonClicked}
color={'none'}
disabled={disabled}
className={clsx(
'bg-canvas-200 hover:bg-canvas-300',
'border-canvas-300 flex items-center gap-1.5 rounded-lg border px-2 py-2 text-sm text-primary-700 transition-colors hover:border-primary-400 hover:bg-primary-50',
disabled && 'opacity-50 cursor-not-allowed',
)}
disabled={disabled}
>
<BiEnvelope className={clsx('h-5 w-5', includeLabel && 'mr-2')} />{' '}
{includeLabel && <>{t('send_message.button_label', 'Message')}</>}
</Button>
</button>
)}
</Tooltip>

View File

@@ -311,7 +311,7 @@ export const OptionalProfileUserForm = (props: {
return (
<>
<Col className={'gap-8'}>
<Col className={'gap-8 max-w-3xl'}>
<p className={'guidance'}>
{t(
'profile.optional.subtitle',

View File

@@ -1,7 +1,6 @@
import {Profile} from 'common/profiles/profile'
import {User} from 'common/user'
import {useState} from 'react'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {Modal} from 'web/components/layout/modal'
import {ShareProfileButtons} from 'web/components/widgets/share-profile-button'
@@ -75,9 +74,25 @@ export const ViewProfileCardButton = (props: {
const username = user.username
return (
<>
<Button onClick={() => setOpen(true)} className={'bg-canvas-50 '}>
{t('share_profile.view_profile_card', 'View Profile Card')}
</Button>
<button
onClick={() => setOpen(true)}
className="border-canvas-300 flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm text-ink-500 transition-colors hover:border-primary-400 hover:bg-primary-50"
style={{
fontSize: '13.5px',
}}
>
<svg
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="h-3.5 w-3.5 hidden sm:block"
>
<rect x="2.5" y="5" width="15" height="11" rx="2" />
<path d="M2.5 9h15" />
</svg>
{t('share_profile.view_profile_card', 'Profile Card')}
</button>
<Modal open={open} setOpen={setOpen} size={'lg'} className={''}>
<Col className="gap-4 bg-canvas-100/75 rounded-2xl justify-center">
<ProfileCardViewer user={user} profile={profile} width={width} height={height} />

View File

File diff suppressed because it is too large Load Diff

View File

@@ -65,7 +65,7 @@ export default function ProfileCarousel(props: {profile: Profile; refreshProfile
src={url}
height={300}
width={300}
className="h-full w-full cursor-pointer rounded object-cover"
className="h-full w-full cursor-pointer rounded-xl object-cover"
autoPlay
muted
loop
@@ -83,7 +83,7 @@ export default function ProfileCarousel(props: {profile: Profile; refreshProfile
width={300}
sizes="(max-width: 640px) 100vw, 300px"
alt=""
className="h-full cursor-pointer rounded object-cover"
className="h-full cursor-pointer rounded-xl object-cover"
onClick={() => {
setLightboxUrl(url)
setLightboxOpen(true)
@@ -91,9 +91,11 @@ export default function ProfileCarousel(props: {profile: Profile; refreshProfile
/>
)}
</div>
<p className="mt-2 px-4 py-1 text-sm w-[300px] whitespace-pre-wrap">
{(profile.image_descriptions as Record<string, string>)?.[url]}
</p>
{(profile.image_descriptions as Record<string, string>)?.[url] && (
<p className="mt-2 px-4 py-1 text-sm w-[300px] whitespace-pre-wrap">
{(profile.image_descriptions as Record<string, string>)?.[url]}
</p>
)}
</Col>
))}
</Carousel>

View File

@@ -12,8 +12,6 @@ import {useLiveCommentsOnProfile} from 'web/hooks/use-comments-on-profile'
import {updateProfile} from 'web/lib/api'
import {useT} from 'web/lib/locale'
import {Subtitle} from './widgets/profile-subtitle'
export const ProfileCommentSection = (props: {
onUser: User
profile: Profile
@@ -26,14 +24,14 @@ export const ProfileCommentSection = (props: {
const parentComments = comments.filter((c) => !c.replyToCommentId)
const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_')
const [profile, setProfile] = useState<Profile>(props.profile)
const [showCommentInput, setShowCommentInput] = useState(false)
const isCurrentUser = currentUser?.id === onUser.id
if (!currentUser && (!profile.comments_enabled || parentComments.length == 0)) return null
return (
<Col className={'rounded'}>
<Row className={'mb-4 justify-between'}>
<Subtitle>{t('profile.comments.section_title', 'Endorsements')}</Subtitle>
<Row className={'justify-between'}>
{isCurrentUser && !simpleView && (
<Tooltip
text={(profile.comments_enabled ? 'Disable' : 'Enable') + ' endorsements from others'}
@@ -61,7 +59,7 @@ export const ProfileCommentSection = (props: {
<>
{currentUser && profile.comments_enabled && (
<>
<div className="mb-4">
<div className="mb-4 text-ink-600">
{isCurrentUser ? (
<>
{t(
@@ -79,11 +77,30 @@ export const ProfileCommentSection = (props: {
)}
</div>
{!isCurrentUser && (
<ProfileCommentInput
className="mb-4 mr-px mt-px"
onUserId={onUser.id}
trackingLocation={'contract page'}
/>
<>
{!showCommentInput ? (
<button
onClick={() => setShowCommentInput(true)}
className="w-fit border-canvas-300 text-primary-700 bg-canvas-200 hover:bg-canvas-300 mb-4 rounded-full border px-4 py-2 text-sm transition-colors"
style={{
padding: '6px 16px',
borderRadius: '100px',
fontSize: '13px',
fontWeight: '400',
letterSpacing: '0.01em',
borderWidth: '1px',
}}
>
{t('profile.comments.write_button', 'Write public endorsement')}
</button>
) : (
<ProfileCommentInput
className="mb-4 mr-px mt-px"
onUserId={onUser.id}
trackingLocation={'contract page'}
/>
)}
</>
)}
</>
)}

View File

@@ -334,6 +334,7 @@ function ProfilePreview(props: {
'hover:before:opacity-100',
'hover:bg-canvas-100',
'relative z-10 cursor-pointer group block rounded-lg overflow-hidden bg-transparent h-full border border-canvas-300',
'text-ink-600',
// hover,
)}
>
@@ -405,7 +406,12 @@ function ProfilePreview(props: {
?.slice(0, 10)
?.map(capitalizePure)
?.map((tag, i) => (
<span key={i} className={'bg-canvas-200 text-sm px-3 py-2 rounded-full'}>
<span
key={i}
className={
'bg-canvas-200 text-primary-700 text-sm px-3 p-1 rounded-full border border-canvas-300'
}
>
{tag.trim()}
</span>
))}

View File

@@ -6,7 +6,6 @@ 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 {Subtitle} from 'web/components/widgets/profile-subtitle'
import {Tooltip} from 'web/components/widgets/tooltip'
import {useProfile} from 'web/hooks/use-profile'
import {useUser} from 'web/hooks/use-user'
@@ -83,10 +82,8 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
if (isCurrentUser || !currentUser) return null
return (
<Col className="w-full gap-6 rounded-xl shadow-sm">
<div className="border-y border-canvas-200 p-2 pb-8">
<Subtitle className="mb-4">{t('profile.connect.title', 'Connect')}</Subtitle>
<Col className="w-full gap-6 rounded-xl">
<div className="py-2">
{/* Primary Action */}
<div className="mb-6">
{profile.allow_direct_messaging || matches.length > 0 ? (
@@ -109,9 +106,9 @@ export function ConnectActions(props: {profile: Profile; user: User}) {
{/* Interest Section */}
<div className="prose prose-neutral dark:prose-invert">
<div className="text-ink-700 font-medium text-lg">
<h2 className="font-medium text-xl">
{t('profile.connect.private_connection_signal', 'Private connection signal')}
</div>
</h2>
{profile.allow_interest_indicating ? (
<>

View File

@@ -8,11 +8,11 @@ import clsx from 'clsx'
import {debug} from 'common/logger'
import {Profile} from 'common/profiles/profile'
import {User, UserActivity} from 'common/user'
import Image from 'next/image'
import Link from 'next/link'
import Router from 'next/router'
import React from 'react'
import toast from 'react-hot-toast'
import {Button} from 'web/components/buttons/button'
import {MoreOptionsUserButton} from 'web/components/buttons/more-options-user-button'
import DropdownMenu from 'web/components/comments/dropdown-menu'
import {Col} from 'web/components/layout/col'
@@ -52,52 +52,82 @@ export default function ProfileHeader(props: {
})
return (
<Row className="w-full">
<Col className="w-full">
{currentUser && !isCurrentUser && isHiddenFromMe && (
<div className="guidance">
{t(
'profile_grid.hidden_notice',
"You hid this person, so they don't appear in your search results.",
)}
</div>
)}
{currentUser && isCurrentUser && disabled && (
<div className="text-red-500">
{t(
'profile.header.disabled_notice',
'You disabled your profile, so no one else can access it.',
)}
</div>
)}
<Row className={clsx('flex-wrap justify-between gap-2 py-1')}>
<Row className="items-center gap-1">
<Col className="gap-1">
<Row className="items-center gap-1 text-xl" data-testid="profile-display-name-age">
{/*{!isCurrentUser && <OnlineIcon last_online_time={userActivity?.last_online_time}/>}*/}
<span>
{simpleView ? (
<Link className={linkClass} href={`/${user.username}`}>
<span className="font-semibold">{user.name}</span>
</Link>
) : (
<span className="font-semibold">{user.name}</span>
)}
</span>
<Row className={'flex-wrap gap-4'}>
{currentUser && !isCurrentUser && isHiddenFromMe && (
<div className="guidance">
{t(
'profile_grid.hidden_notice',
"You hid this person, so they don't appear in your search results.",
)}
</div>
)}
{currentUser && isCurrentUser && disabled && (
<div className="text-red-500">
{t(
'profile.header.disabled_notice',
'You disabled your profile, so no one else can access it.',
)}
</div>
)}
<Col>
<Row className="w-full gap-6 flex-wrap">
{profile.pinned_url && (
<div className="h-[108px] w-[108px] flex-none">
<Image
priority={true}
src={profile.pinned_url}
height={300}
width={300}
sizes="(max-width: 640px) 100vw, 300px"
alt=""
className="h-full w-full rounded-2xl object-cover"
/>
</div>
)}
<Col className="max-w-[160px] sm:max-w-[300px]">
<Row className={clsx('flex-wrap justify-between gap-2 py-1')}>
<Row className="items-center gap-1">
<Col className="gap-1">
<Row className="items-center gap-1" data-testid="profile-display-name-age">
{/*{!isCurrentUser && <OnlineIcon last_online_time={userActivity?.last_online_time}/>}*/}
<span>
{simpleView ? (
<Link className={linkClass} href={`/${user.username}`}>
<span
className="font-cormorant text-4xl font-medium"
style={{lineHeight: '1.1', letterSpacing: '-0.01em'}}
>
{user.name}
</span>
</Link>
) : (
<span
className="font-cormorant text-4xl font-medium"
style={{lineHeight: '1.1', letterSpacing: '-0.01em'}}
>
{user.name}
</span>
)}
</span>
</Row>
<ProfilePrimaryInfo profile={profile} />
</Col>
</Row>
<ProfilePrimaryInfo profile={profile} />
</Col>
</Row>
</Row>
</Col>
</Row>
<Row className={'px-4 gap-2 flex-wrap py-2'} data-testid="profile-keywords">
<Row className={'gap-2 flex-wrap py-2'} data-testid="profile-keywords">
{profile.keywords?.map(capitalizePure)?.map((tag, i) => (
<span
key={i}
className={'bg-canvas-200'}
className={'border-canvas-300 text-primary-700 bg-canvas-200'}
style={{
padding: '6px 16px',
borderRadius: '20px',
padding: '5px 13px',
borderRadius: '100px',
fontSize: '13px',
fontWeight: '400',
letterSpacing: '0.01em',
borderWidth: '1px',
}}
>
{tag.trim()}
@@ -106,8 +136,31 @@ export default function ProfileHeader(props: {
</Row>
</Col>
{profile.headline && (
<div className="italic max-w-3xl px-4 py-3" data-testid="profile-headline">
"{profile.headline}"
<div
className="relative max-w-xl px-4 py-3 text-ink-600 flex items-center justify-center"
data-testid="profile-headline"
style={{
fontSize: '15px',
lineHeight: '1.65',
borderLeft: '1.5px solid rgb(var(--color-primary-300))',
paddingLeft: '40px',
}}
>
<div className="h-fit relative">
<span
className="absolute -mt-6 text-3xl text-primary-300"
style={{fontFamily: 'serif'}}
>
"
</span>
<span className="italic">{profile.headline}</span>
<span
className="absolute -bottom-8 text-3xl text-primary-300"
style={{fontFamily: 'serif'}}
>
"
</span>
</div>
</div>
)}
</Row>
@@ -159,21 +212,20 @@ export function ProfileHeaderActions(props: {
if (currentUser && isCurrentUser) {
return (
<Row className={'items-center gap-4'}>
<Row className={'items-center gap-2'}>
<ViewProfileCardButton user={user} profile={profile} />
<ShareProfileButton className="sm:flex" username={user.username} />
<Tooltip text={t('more_options_user.edit_profile', 'Edit profile')} noTap>
<Button
<button
data-testid="profile-edit"
color={'gray-outline'}
onClick={() => {
track('editprofile', {userId: user.id})
Router.push('profile')
}}
size="sm"
className="border-canvas-300 flex items-center gap-1.5 rounded-lg border px-2 py-2 text-sm text-primary-700 transition-colors hover:border-primary-400 hover:bg-primary-50"
>
<PencilIcon className=" h-4 w-4" />
</Button>
<PencilIcon className="h-4 w-4 text-ink-500" />
</button>
</Tooltip>
<Tooltip
@@ -183,7 +235,11 @@ export function ProfileHeaderActions(props: {
>
<DropdownMenu
menuWidth={'w-52'}
icon={<EllipsisHorizontalIcon className="h-5 w-5" aria-hidden="true" />}
icon={
<button className="border-canvas-300 flex items-center gap-1.5 rounded-lg border px-2 py-2 text-sm text-primary-700 transition-colors hover:border-primary-400 hover:bg-primary-50">
<EllipsisHorizontalIcon className="h-4 w-4 text-ink-500" aria-hidden="true" />
</button>
}
items={[
{
name:
@@ -239,7 +295,7 @@ export function ProfileHeaderActions(props: {
}
return (
<Row className="items-center gap-1 sm:gap-2">
<Row className="items-center gap-2">
<ViewProfileCardButton user={user} profile={profile} />
<ShareProfileButton className="sm:flex" username={user.username} />
{currentUser && (

View File

@@ -3,7 +3,6 @@ import clsx from 'clsx'
import {debug} from 'common/logger'
import {Profile} from 'common/profiles/profile'
import {UserActivity} from 'common/user'
import Image from 'next/image'
import React, {ReactNode} from 'react'
import {ProfileAnswers} from 'web/components/answers/profile-answers'
import {ProfileBio} from 'web/components/bio/profile-bio'
@@ -12,10 +11,16 @@ import {Row} from 'web/components/layout/row'
import {SignUpButton} from 'web/components/nav/sidebar'
import {ConnectActions} from 'web/components/profile/connect-actions'
import ProfileHeader, {ProfileHeaderActions} from 'web/components/profile/profile-header'
import ProfileAbout from 'web/components/profile-about'
import ProfileAbout, {
ProfileInterestsAndCauses,
ProfileLinks,
ProfilePersonality,
} from 'web/components/profile-about'
import ProfileCarousel from 'web/components/profile-carousel'
import {ProfileCommentSection} from 'web/components/profile-comment-section'
import {Content} from 'web/components/widgets/editor'
import {Subtitle} from 'web/components/widgets/subtitle'
import {shortenName} from 'web/components/widgets/user-link'
import {useGetter} from 'web/hooks/use-getter'
import {useHiddenProfiles} from 'web/hooks/use-hidden-profiles'
import {useUser} from 'web/hooks/use-user'
@@ -74,11 +79,19 @@ export function ProfileInfo(props: {
const {data: userActivity} = useUserActivity(user?.id)
// const isCurrentUser = currentUser?.id === user.id
return (
<>
<div className="bg-canvas-50 border-canvas-300 mb-6 flex items-center gap-2 border-b px-8 py-3">
<div
className="bg-canvas-50 border-canvas-300 mb-6 flex items-center border-b px-4 sm:px-9 py-4"
style={{
borderBottomWidth: '1px',
gap: '10px',
}}
>
<div className="flex w-full items-center gap-2">
<BackButton />
<BackButton className={'hidden sm:flex'} />
<div className="ml-auto flex items-center gap-2">
<ProfileHeaderActions
@@ -96,22 +109,36 @@ export function ProfileInfo(props: {
</div>
</div>
<div className="mx-auto w-full px-8 pb-6 pt-0">
<Row className="items-start gap-6">
{profile.pinned_url && (
<div className="h-[108px] w-[108px] flex-none">
<Image
priority={true}
src={profile.pinned_url}
height={300}
width={300}
sizes="(max-width: 640px) 100vw, 300px"
alt=""
className="h-full w-full cursor-pointer rounded-2xl object-cover"
/>
</div>
)}
<div className="min-w-0 flex-1">
<div className="mx-auto w-full px-4 sm:px-8 pb-6 pt-0">
<Row className="relative items-start gap-6">
{/* Gradient overlay */}
{/*<div*/}
{/* style={{*/}
{/* position: 'absolute',*/}
{/* inset: 0,*/}
{/* pointerEvents: 'none',*/}
{/* background: `radial-gradient(ellipse 55% 70% at 100% 30%, rgba(193,127,62,0.07) 0%, transparent 65%), radial-gradient(ellipse 30% 40% at 0% 90%, rgba(193,127,62,0.05) 0%, transparent 55%)`,*/}
{/* }}*/}
{/*/>*/}
{/* First letter of name */}
<div
style={{
position: 'absolute',
right: '0%',
top: '40%',
transform: 'translateY(-50%)',
fontFamily: "'Cormorant Garamond', serif",
fontSize: 'clamp(7rem, 14vw, 16rem)',
fontWeight: 500,
color: 'rgba(193,127,62,0.04)',
lineHeight: 1,
userSelect: 'none',
pointerEvents: 'none',
}}
>
{user.name?.charAt(0).toUpperCase()}
</div>
<div className={clsx('min-w-0 flex-1', !profile.pinned_url && 'ml-6')}>
<ProfileHeader
user={user}
userActivity={userActivity}
@@ -177,8 +204,15 @@ export function ProfileInfo(props: {
function ProfileCard(props: {title?: ReactNode; children: ReactNode; className?: string}) {
const {title, children, className} = props
// Check if children is null or undefined
if (children == null) {
return null
}
return (
<div className={clsx('bg-canvas-50 border-canvas-300 rounded-2xl border p-6', className)}>
<div
className={clsx('bg-canvas-50 border-canvas-300 border', className)}
style={{borderRadius: '14px', padding: '22px 24px'}}
>
{title != null && <CardTitle>{title}</CardTitle>}
{children}
</div>
@@ -188,9 +222,20 @@ function ProfileCard(props: {title?: ReactNode; children: ReactNode; className?:
function CardTitle(props: {children: ReactNode; className?: string}) {
const {children, className} = props
return (
<div className={clsx('text-ink-900 mb-4 text-lg font-semibold tracking-tight', className)}>
// <div
// className={clsx(
// 'text-ink-900 mb-3.5 font-cormorant text-xl font-medium tracking-wide',
// className,
// )}
// style={{letterSpacing: '0.01em'}}
// >
// {children}
// </div>
<Subtitle
className={clsx('!mt-0 !mb-4 font-cormorant text-xl font-medium tracking-wide', className)}
>
{children}
</div>
</Subtitle>
)
}
@@ -223,12 +268,38 @@ function ProfileContent(props: {
const currentUser = useUser()
const isCurrentUser = currentUser?.id === user.id
const t = useT()
return (
<>
<div className="mt-4 grid grid-cols-1 items-start gap-6 lg:grid-cols-[1fr_480px]">
<div className="mt-4 grid grid-cols-1 items-start gap-6 lg:grid-cols-[1fr_800px]">
<Col className="gap-6">
<ProfileCard className="p-5">
<ProfileCard title="Details" className="p-5">
<ProfileAbout
profile={profile}
userActivity={userActivity}
isCurrentUser={isCurrentUser}
/>
</ProfileCard>
<ProfileCard title={t('profile.interests_and_causes', 'Interests')} className="p-5">
<ProfileInterestsAndCauses profile={profile} />
</ProfileCard>
{(profile.mbti || profile.big5_agreeableness) && (
<ProfileCard title={t('profile.personality', 'Personality')} className="p-5">
<ProfilePersonality profile={profile} />
</ProfileCard>
)}
{profile.links && Object.keys(profile.links).length > 0 && (
<ProfileCard title={t('profile.links', 'Links')} className="p-5">
<ProfileLinks profile={profile} />
</ProfileCard>
)}
</Col>
<Col className="gap-6">
<ProfileCard title={t('profile.bio.about_me', 'About Me')} className="p-0">
<ProfileBio
isCurrentUser={isCurrentUser}
profile={profile}
@@ -238,12 +309,21 @@ function ProfileContent(props: {
</ProfileCard>
{isProfileVisible && (
<ProfileCard className="p-5">
<ProfileCarousel profile={profile} refreshProfile={refreshProfile} />
</ProfileCard>
// <ProfileCard className="!p-0">
<ProfileCarousel profile={profile} refreshProfile={refreshProfile} />
// </ProfileCard>
)}
<ProfileCard className="p-5">
<ProfileCard
className="p-5"
title={
isCurrentUser
? t('answers.display.your_prompts', 'Compatibility Prompts')
: t('answers.display.user_prompts', 'Compatibility Prompts', {
name: shortenName(user.name),
})
}
>
<ProfileAnswers
isCurrentUser={isCurrentUser}
user={user}
@@ -253,11 +333,7 @@ function ProfileContent(props: {
/>
</ProfileCard>
<ProfileCard className="p-5">
<ConnectActions user={user} profile={profile} />
</ProfileCard>
<ProfileCard className="p-5">
<ProfileCard className="p-5" title={t('profile.comments.section_title', 'Endorsements')}>
<ProfileCommentSection
onUser={user}
profile={profile}
@@ -265,16 +341,12 @@ function ProfileContent(props: {
simpleView={!!fromProfilePage}
/>
</ProfileCard>
</Col>
<Col className="gap-6">
<ProfileCard title="Details" className="p-5">
<ProfileAbout
profile={profile}
userActivity={userActivity}
isCurrentUser={isCurrentUser}
/>
</ProfileCard>
{!isCurrentUser && currentUser && (
<ProfileCard title={t('profile.connect.title', 'Connect')}>
<ConnectActions user={user} profile={profile} />
</ProfileCard>
)}
</Col>
</div>
{/*<LikesDisplay*/}

View File

@@ -1,7 +1,9 @@
import {getLocationText} from 'common/geodb'
import {getGoogleMapsUrl, getLocationText} from 'common/geodb'
import {Profile} from 'common/profiles/profile'
import React from 'react'
import {IoLocationOutline} from 'react-icons/io5'
import {IconWithInfo} from 'web/components/icons'
import {CustomLink} from 'web/components/links'
export function ProfileLocation(props: {profile: Profile; prefix?: string}) {
const {profile, prefix} = props
@@ -12,5 +14,13 @@ export function ProfileLocation(props: {profile: Profile; prefix?: string}) {
return null
}
return <IconWithInfo text={text} icon={<IoLocationOutline className="h-4 w-4" />} />
return (
<IconWithInfo
icon={<IoLocationOutline className="text-ink-300" style={{width: '14px', height: '14px'}} />}
>
<CustomLink href={getGoogleMapsUrl(text)} className={'hover:text-primary-500'}>
{text}
</CustomLink>
</IconWithInfo>
)
}

View File

@@ -17,26 +17,36 @@ export default function ProfilePrimaryInfo(props: {profile: Profile; short?: boo
const t = useT()
const {measurementSystem} = useMeasurementSystem()
return (
<Row className="text-ink-700 gap-4 text-sm" data-testid="profile-gender-location-height-inches">
<Row
className="text-ink-500 gap-4 flex-wrap"
data-testid="profile-gender-location-height-inches"
style={{fontSize: '13.5px', gap: '6px 18px'}}
>
<ProfileLocation profile={profile} />
{!short && profile.gender && (
{profile.age && (
<IconWithInfo
text={capitalize(
t(`profile.gender.${profile.gender}`, convertGender(profile.gender as Gender)),
)}
icon={<GenderIcon gender={profile.gender as Gender} className="h-4 w-4 " />}
text={t('profile.header.age', '{age} years old', {age: profile.age})}
icon={<Calendar className="text-ink-300" style={{width: '14px', height: '14px'}} />}
/>
)}
{!short && profile.height_in_inches != null && (
<IconWithInfo
text={formatProfileValue('height_in_inches', profile.height_in_inches, measurementSystem)}
icon={<MdHeight className="h-4 w-4 " />}
icon={<MdHeight className="text-ink-300" style={{width: '14px', height: '14px'}} />}
/>
)}
{profile.age && (
{!short && profile.gender && (
<IconWithInfo
text={t('profile.header.age', '{age} years old', {age: profile.age})}
icon={<Calendar className="h-4 w-4 " />}
text={capitalize(
t(`profile.gender.${profile.gender}`, convertGender(profile.gender as Gender)),
)}
icon={
<GenderIcon
gender={profile.gender as Gender}
className="text-ink-300"
// style={{width: '14px', height: '14px'}}
/>
}
/>
)}
</Row>

View File

@@ -9,7 +9,15 @@ export default function SiteLogo(props: {noLink?: boolean; className?: string})
const inner = (
<>
<FavIconBlack className={className?.includes('invert') ? '' : 'dark:invert'} />
<div className={clsx('my-auto text-xl font-thin logo')}>
<div
className={clsx('my-auto logo')}
style={{
fontFamily: 'Cormorant Garamond',
fontSize: '22px',
fontWeight: 500,
letterSpacing: '0.03em',
}}
>
{IS_PROD ? 'Compass' : 'Compass dev'}
</div>
</>

View File

@@ -41,15 +41,18 @@ export function UserHandles(props: {links: Socials; className?: string}) {
return (
<Row
className={clsx('text-ink-400 flex-wrap items-center gap-2 sm:gap-x-4', className)}
className={clsx('flex-wrap items-center gap-2', className)}
data-testid="profile-social-media-accounts"
>
{display.map(({platform, label, url}) => (
<a key={platform} target="_blank" href={url}>
<Row className="items-center gap-1">
<SocialIcon site={platform as any} className="text-primary-900 h-4 w-4" />
<span className="text-primary-700 text-sm hover:text-primary-500">{label}</span>
</Row>
<a
key={platform}
target="_blank"
href={url}
className="border-canvas-300 bg-canvas-0 flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-[12.5px] text-ink-500 transition-colors hover:border-primary-300 hover:text-primary-600"
>
<SocialIcon site={platform as any} className="text-ink-500 h-[16px] w-[16px]" />
<span>{label}</span>
</a>
))}
</Row>

View File

@@ -36,7 +36,7 @@ export function VoteItem(props: {vote: Vote; onVoted?: () => void | Promise<void
const t = useT()
// console.debug('creator', creator, vote)
return (
<Col className={'mb-4 rounded-lg border border-canvas-200 p-4'}>
<Col className={'mb-4 rounded-lg border border-canvas-200 p-4 bg-canvas-50'}>
<Row className={'mb-2'}>
<Col className={'flex-grow'}>
<p className={'text-2xl'}>{vote.title}</p>

View File

@@ -25,7 +25,7 @@ export function Carousel(props: {
<Row
className={clsx(
'scrollbar-hide w-full snap-x overflow-x-auto scroll-smooth',
labelsParentClassName ?? 'gap-4',
labelsParentClassName ?? 'gap-2',
)}
ref={ref}
onScroll={onScroll}

View File

@@ -69,7 +69,8 @@ const proseClass = (size: 'sm' | 'md' | 'lg') =>
size === 'sm' ? 'prose-sm' : 'text-md',
size !== 'lg' && 'prose-p:my-0 prose-ul:my-0 prose-ol:my-0 prose-li:my-0',
'[&>p]:prose-li:my-0',
'text-ink-900 prose-blockquote:text-teal-700',
'prose-h1:text-ink-900 prose-h2:text-ink-900 prose-h3:text-ink-900',
'text-ink-500 prose-blockquote:text-teal-700 ',
'break-anywhere',
)

View File

@@ -2,5 +2,5 @@ import clsx from 'clsx'
export function Subtitle(props: {children: string; className?: string}) {
const {children: text, className} = props
return <h2 className={clsx('text-ink-600 inline-block font-semibold', className)}>{text}</h2>
return <h2 className={clsx('text-ink-900 inline-block font-semibold', className)}>{text}</h2>
}

View File

@@ -25,12 +25,17 @@ export const ShareProfileButton = (props: {
return (
<CopyLinkOrShareButton
className={className}
className={clsx(
className,
'border-canvas-300 flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm text-ink-500 transition-colors hover:border-primary-400 hover:bg-primary-50',
)}
url={shareUrl}
eventTrackingName="shareprofile"
color={color}
size="sm"
iconClassName={'hidden sm:inline'}
>
<div className="ml-2 text-sm">{t('button.share.label', 'Copy profile link')}</div>
<div className="text-sm">{t('button.share.label', 'Copy Link')}</div>
</CopyLinkOrShareButton>
)
}

View File

@@ -49,7 +49,7 @@ export const StarButton = (props: {
>
<StarIcon
className={clsx(
'h-8 w-8 transition-colors group-hover:fill-yellow-400/70',
'h-7 w-7 transition-colors group-hover:fill-yellow-400/70',
isStarred && 'fill-yellow-400 stroke-yellow-500 dark:stroke-yellow-600',
)}
/>

View File

@@ -5,7 +5,7 @@ export function Subtitle(props: {children: React.ReactNode; className?: string})
return (
<h2
className={clsx(
'text-primary-700 mb-2 mt-6 inline-block text-lg sm:mb-2 sm:mt-6 sm:text-xl',
'text-ink-700 mb-2 mt-6 inline-block text-lg sm:mb-2 sm:mt-6 sm:text-2xl',
className,
)}
>

View File

@@ -4,7 +4,7 @@ import {capitalize} from 'lodash'
import {convertRelationshipType, RelationshipType} from 'web/lib/util/convert-types'
import stringOrStringArrayToText from 'web/lib/util/string-or-string-array-to-text'
export function getSeekingConnectionText(profile: Profile, t: any, short?: boolean) {
export function getSeekingConnectionText(profile: Profile, t: any, _short?: boolean) {
const relationshipTypes = profile.pref_relation_styles
let seekingGenderText = stringOrStringArrayToText({
text: relationshipTypes?.length
@@ -17,7 +17,7 @@ export function getSeekingConnectionText(profile: Profile, t: any, short?: boole
)
.sort()
: [t('profile.connection.default', 'connection')],
preText: !short ? t('profile.seeking', 'Seeking') : undefined,
// preText: !short ? t('profile.seeking', 'Seeking') : undefined,
asSentence: true,
capitalizeFirstLetterOption: false,
t: t,

View File

@@ -25,6 +25,10 @@ export default function Document() {
// href="https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&display=swap"
rel="stylesheet"
/>
{/* PWA primary color */}
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />

View File

@@ -10,156 +10,303 @@ export const config = {runtime: 'edge'}
const COMPASS_LOGO =
'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-512.png?alt=media&token=d2fa566f-f443-4a94-90be-e50403f1805a'
export const getCardOptions = async () => ({
width: 1200,
height: 630,
})
export const getCardOptions = async () => ({width: 1200, height: 630})
// Edge-safe capitalize
function capitalize(str: string) {
if (!str) return ''
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}
// Palette
const C = {
ink900: '#1E1A14',
ink600: '#786C5C',
ink500: '#8C8070',
ink300: '#BEB2A2',
canvas50: '#F7F4EF',
canvas100: '#EDE8E0',
canvas200: '#E8D5BC',
canvas300: '#DECBB2',
canvas950: '#2C2416',
primary50: '#FAF3E9',
primary100: '#F3E4CE',
primary200: '#E8C99D',
primary300: '#DCAB71',
primary400: '#D09352',
primary500: '#C17F3E',
primary600: '#A6682E',
primary700: '#855022',
primary800: '#653A18',
}
function OgProfile(props: ogProps) {
console.log(props)
const {avatarUrl, name, city, country, age, interests, keywords} = props
let headline = props.headline
const _interestsList =
typeof interests === 'string' ? (interests ? interests.split(',') : []) : (interests ?? [])
const keywordsList =
typeof keywords === 'string' ? (keywords ? keywords.split(',') : []) : (keywords ?? [])
const allTags = [...keywordsList].filter(Boolean).slice(0, 8)
const interestsList =
typeof interests === 'string' ? (interests ? interests.split(',') : []) : (interests ?? [])
const allTags = [...keywordsList, ...interestsList].filter(Boolean).slice(0, 6)
const maxChars = 220
const maxChars = 250
if (headline && headline.length > maxChars) {
headline = headline.slice(0, maxChars) + '...'
headline = headline.slice(0, maxChars) + ''
}
const totalChars = (headline?.length || 0) + (allTags?.join(' ')?.length || 0) + name.length * 3
const hasLongContent = (headline?.length || 0) > 80 || allTags.length > 4
const imgSize = 300
const isLargerPicLayout = totalChars < maxChars
const imgSize = isLargerPicLayout ? 400 : 250
return (
<div
style={{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
padding: '50px',
fontFamily: 'sans-serif',
backgroundColor: '#f5f5f5',
backgroundColor: C.canvas100,
fontFamily: 'Georgia, serif',
position: 'relative',
}}
>
<div style={{display: 'flex', flex: isLargerPicLayout ? 1 : 3}}>
{/* Left Column: Text */}
{/* Left dark panel */}
<div
style={{
width: '340px',
height: '630px',
backgroundColor: C.canvas950,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '20px',
flexShrink: 0,
position: 'relative',
}}
>
{/* Amber ring behind avatar */}
<div
style={{
flex: isLargerPicLayout ? 1 : 3,
width: `${imgSize + 10}px`,
height: `${imgSize + 10}px`,
borderRadius: '50%',
backgroundColor: C.primary700,
display: 'flex',
flexDirection: 'column',
gap: '10px',
justifyContent: 'center',
}}
>
<div style={{display: 'flex', fontSize: '64px', fontWeight: 'bold'}}>
{name}
{age && `, ${age}`}
</div>
{city && (
<div
style={{
display: 'flex',
fontSize: '28px',
marginBottom: '20px',
marginTop: '20px',
opacity: 0.85,
}}
>
{city}
{country && `, ${country}`}
</div>
)}
{/*<div style={{display: 'flex', justifyContent: 'flex-end', alignItems: 'flex-start'}}>*/}
{/* <img src={'https://www.compassmeet.com/favicon-black.svg'} width={100} height={100} />*/}
{/*</div>*/}
{allTags && (
<div style={{display: 'flex', gap: '10px', flexWrap: 'wrap'}}>
{allTags?.map(capitalize).map((tag, i) => (
<span
key={i}
style={{
padding: '8px 16px',
backgroundColor: '#ddd',
borderRadius: '20px',
fontSize: '24px',
}}
>
{tag.trim()}
</span>
))}
</div>
)}
{isLargerPicLayout && (
<div style={{display: 'flex'}}>
{headline && (
<div style={{display: 'flex', fontSize: '36px', marginTop: '40px'}}>{headline}</div>
)}
</div>
)}
</div>
{/* Right Column: Avatar */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
src={avatarUrl || COMPASS_LOGO}
width={imgSize}
height={imgSize}
style={{borderRadius: 50, objectFit: 'cover'}}
style={{borderRadius: '50%', objectFit: 'cover', display: 'flex'}}
alt="Avatar"
/>
{isLargerPicLayout && (
<div
style={{
display: 'flex',
fontSize: '48px',
fontWeight: 'semibold',
fontFamily: 'Georgia',
marginTop: '20px',
fontStyle: 'italic',
}}
>
compassmeet.com
</div>
)}
</div>
</div>
{!isLargerPicLayout && (
{/* Compass URL */}
<div
style={{
flex: 2,
display: 'flex',
flexDirection: 'column',
marginBottom: '40px',
alignItems: 'center',
gap: '8px',
marginTop: '8px',
}}
>
{headline && (
<div style={{display: 'flex', fontSize: '36px', marginTop: '40px'}}>{headline}</div>
<div
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: C.primary400,
display: 'flex',
}}
/>
<span
style={{
fontFamily: 'Georgia, serif',
fontStyle: 'italic',
fontSize: '22px',
color: C.primary300,
display: 'flex',
}}
>
compassmeet.com
</span>
<div
style={{
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: C.primary400,
display: 'flex',
}}
/>
</div>
{/* Subtle bottom accent line */}
<div
style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
height: '4px',
backgroundColor: C.primary500,
display: 'flex',
}}
/>
</div>
{/* Right content area */}
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '52px 56px',
gap: '0px',
position: 'relative',
}}
>
{/* Top accent line */}
<div
style={{
position: 'absolute',
top: '0',
left: '0',
right: '0',
height: '4px',
backgroundColor: C.primary500,
display: 'flex',
}}
/>
{/* Name + age */}
<div
style={{
display: 'flex',
alignItems: 'baseline',
gap: '16px',
marginBottom: '10px',
}}
>
<span
style={{
fontFamily: 'Georgia, serif',
fontSize: hasLongContent ? '70px' : '80px',
fontWeight: 'bold',
color: C.ink900,
lineHeight: 1.05,
display: 'flex',
}}
>
{name}
</span>
{age && (
<span
style={{
fontSize: hasLongContent ? '38px' : '44px',
color: C.ink500,
fontFamily: 'Georgia, serif',
display: 'flex',
}}
>
{age}
</span>
)}
</div>
)}
{/* Location */}
{(city || country) && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginBottom: '24px',
}}
>
<div
style={{
width: '5px',
height: '5px',
borderRadius: '50%',
backgroundColor: C.primary400,
display: 'flex',
flexShrink: 0,
marginTop: '2px',
}}
/>
<span
style={{
fontSize: '26px',
color: C.ink500,
fontFamily: 'Georgia, serif',
fontStyle: 'italic',
display: 'flex',
}}
>
{[city, country].filter(Boolean).join(', ')}
</span>
</div>
)}
{/* Tags */}
{allTags.length > 0 && (
<div
style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap',
marginBottom: '24px',
}}
>
{allTags.map(capitalize).map((tag, i) => (
<span
key={i}
style={{
padding: '6px 18px',
backgroundColor: C.canvas200,
color: C.primary700,
borderRadius: '100px',
fontSize: '22px',
fontFamily: 'Georgia, serif',
display: 'flex',
}}
>
{tag.trim()}
</span>
))}
</div>
)}
{/* Headline */}
{headline && (
<div
style={{
display: 'flex',
borderLeft: `3px solid ${C.primary300}`,
paddingLeft: '20px',
marginTop: '4px',
}}
>
<span
style={{
fontSize: '28px',
color: C.ink600,
fontFamily: 'Georgia, serif',
fontStyle: 'italic',
lineHeight: 1.5,
display: 'flex',
}}
>
{headline}
</span>
</div>
)}
</div>
</div>
)
}
@@ -169,7 +316,6 @@ export default async function handler(req: NextRequest) {
const {searchParams} = new URL(req.url)
const options = await getCardOptions()
// Clean search params by removing 'amp;' prefixes that occur due to URL encoding
const cleanedEntries = Array.from(searchParams.entries()).map(([key, value]) => [
key.replace(/^amp;/, ''),
value,

View File

@@ -113,8 +113,8 @@ export default function EventsPage() {
</div>
{/* Event Ideas Section */}
<div className="mt-6 bg-canvas-100 rounded-lg p-4">
<h2 className="text-lg font-semibold mb-2 mt-0">
<div className="mt-6 bg-canvas-50 border border-canvas-200 rounded-lg p-4">
<h2 className="text-xl font-semibold mb-2 mt-0">
{t('events.why_organize', 'Why organize events?')}
</h2>
<p className="text-ink-600 text-sm mb-3">
@@ -124,28 +124,28 @@ export default function EventsPage() {
)}
</p>
<div className="flex flex-wrap gap-2">
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
📚 {t('events.book_clubs', 'Book clubs')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🎮 {t('events.game_nights', 'Game nights')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🚶 {t('events.walking_groups', 'Walking groups')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
{t('events.coffee_chats', 'Coffee chats')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🎨 {t('events.creative_workshops', 'Creative workshops')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🤔 {t('events.philosophy_discussions', 'Philosophy discussions')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🌱 {t('events.sustainability_meetups', 'Sustainability meetups')}
</span>
<span className="bg-canvas-50 text-ink-700 px-3 py-1 rounded-full text-xs">
<span className="bg-canvas-100 border border-canvas-200 text-ink-700 px-3 py-1 rounded-full text-xs">
🎯 {t('events.hobby_exchanges', 'Hobby exchanges')}
</span>
</div>

View File

@@ -38,7 +38,7 @@ function SectionCard({icon, title, description, links}: SectionCardProps) {
</div>
{/* Title & description */}
<h2 className="text-base font-bold text-ink-900 mb-2">{title}</h2>
<h2 className="font-bold text-ink-900 mb-2">{title}</h2>
<p className="text-sm text-ink-500 leading-relaxed mb-6">{description}</p>
{/* Links */}

View File

@@ -111,7 +111,7 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
url={`/profile`}
/>
<Col className="items-center">
<BackButton className="-ml-2 mb-2 self-start" />
<BackButton className="ml-2 mb-2 self-start" />
<Col className={'w-full px-6 py-4'}>
<RequiredProfileUserForm
data={baseUser}

View File

@@ -265,7 +265,7 @@ const DataPrivacySettings = () => {
>
{isDownloading
? t('settings.data_privacy.downloading', 'Downloading...')
: t('settings.data_privacy.download', 'Download all my data (JSON)')}
: t('settings.data_privacy.download', 'Download all my data')}
</Button>
</div>
)

View File

@@ -139,7 +139,7 @@ export default function SignupPage() {
onSubmit={async () => advanceToStep(1)}
/>
) : step === 1 ? (
<Col className={'w-full px-2 sm:px-6 py-4 mb-2'}>
<Col className={'w-full px-2 sm:px-6 py-4 mb-2 '}>
<OptionalProfileUserForm
profile={profileForm}
setProfile={setProfileState}

View File

@@ -72,7 +72,7 @@ function SectionCard({icon, title, description, links}: SectionCardProps) {
</div>
{/* Title & description */}
<h2 className="text-base font-bold text-ink-900 mb-1.5">{title}</h2>
<h2 className="font-bold text-ink-900 mb-1.5">{title}</h2>
<p className="text-sm text-ink-500 leading-relaxed mb-5">{description}</p>
{/* Links */}

View File

@@ -66,7 +66,7 @@
}
.logo {
font-family: var(--font-main), serif;
font-family: 'Cormorant Garamond', serif;
}
@layer base {
@@ -103,6 +103,7 @@
/* Ink - Text Colors */
--color-ink-900: 30 26 20; /* Deep Warm Black (#1E1A14) */
--color-ink-500: 140 128 112; /* Muted Warm Gray (#8C8070) */
--color-ink-600: 120 108 92;
/* Green - Accents */
--color-green-500: 107 143 113; /* Accent - Sage Green (#6B8F71) */
@@ -138,7 +139,7 @@
--color-ink-300: 160 160 160;
--color-ink-400: 160 160 160;
/*--color-ink-500: 80 80 80;*/
--color-ink-600: 0 0 0;
/*--color-ink-600: 0 0 0;*/
--color-ink-700: 0 0 0;
--color-ink-800: 0 0 0;
/*--color-ink-900: 0 0 0;*/
@@ -219,6 +220,7 @@
--color-ink-950: 255 255 255; /* Purest highlight */
--color-ink-900: 247 244 239; /* Main Body Text (Old Card color) */
--color-ink-500: 176 160 140; /* Muted Text - shifted to warm tan */
--color-ink-600: 156 140 120;
/* Green - Sage looks great on dark brown */
--color-green-500: 125 160 131; /* Lightened Sage */
@@ -229,7 +231,7 @@
/*--color-ink-900: 255 255 255;*/
--color-ink-800: 255 255 255;
--color-ink-700: 255 255 255;
--color-ink-600: 255 255 255;
/*--color-ink-600: 255 255 255;*/
/*--color-ink-500: 200 200 200;*/
--color-ink-400: 100 100 100;
--color-ink-300: 100 100 100;
@@ -300,29 +302,29 @@
--color-green-900: 20 83 45;
--color-green-950: 5 46 22; /* darkest green */
--color-yellow-50: 255 251 235; /* lightest yellow */
--color-yellow-100: 254 243 199;
--color-yellow-200: 253 230 138;
--color-yellow-300: 252 211 77;
--color-yellow-400: 251 191 36;
--color-yellow-950: 255 251 235; /* lightest yellow */
--color-yellow-900: 254 243 199;
--color-yellow-800: 253 230 138;
--color-yellow-700: 252 211 77;
--color-yellow-600: 251 191 36;
--color-yellow-500: 245 158 11; /* standard yellow */
--color-yellow-600: 217 119 6;
--color-yellow-700: 180 83 9;
--color-yellow-800: 146 64 14;
--color-yellow-900: 113 63 18;
--color-yellow-950: 66 50 3; /* darkest yellow */
--color-yellow-400: 217 119 6;
--color-yellow-300: 180 83 9;
--color-yellow-200: 146 64 14;
--color-yellow-100: 113 63 18;
--color-yellow-50: 66 50 3; /* darkest yellow */
--color-red-50: 254 242 242; /* lightest red */
--color-red-100: 254 226 226;
--color-red-200: 254 202 202;
--color-red-300: 252 165 165;
--color-red-400: 248 113 113;
--color-red-950: 254 242 242; /* lightest red */
--color-red-900: 254 226 226;
--color-red-800: 254 202 202;
--color-red-700: 252 165 165;
--color-red-600: 248 113 113;
--color-red-500: 239 68 68; /* standard red */
--color-red-600: 220 38 38;
--color-red-700: 185 28 28;
--color-red-800: 153 27 27;
--color-red-900: 127 29 29;
--color-red-950: 69 10 10; /* darkest red */
--color-red-400: 220 38 38;
--color-red-300: 185 28 28;
--color-red-200: 153 27 27;
--color-red-100: 127 29 29;
--color-red-50: 69 10 10; /* darkest red */
touch-action: pan-y;
}
@@ -411,7 +413,7 @@ h3,
h4,
h5,
h6 {
/*font-family: 'Inter', sans-serif; !* Clean modern font *!*/
font-family: 'Cormorant Garamond', Inter, sans-serif; /* Clean modern font */
font-weight: 900; /* Semi-bold for clarity */
/*color: #111827; !* Near-black text for readability *!*/
line-height: 1.25;

View File

@@ -27,6 +27,8 @@ module.exports = {
'major-mono': ['var(--font-logo)', 'monospace'],
figtree: ['icomoon', 'var(--font-main)', 'emoji', 'sans-serif'],
'grenze-gotisch': ['var(--font-match-cards)', 'cursive'], // just for match card game
cormorant: ['Cormorant Garamond', 'serif'],
'dm-sans': ['DM Sans', 'sans-serif'],
},
),
extend: {