- {faChild}
-
- {/*
*/}
-
+ const icon =
+ profile.has_kids === 0 ? (
+
-
- ) : faChild
+ ) : (
+ faChild
+ )
return
}
-export const formatProfileValue = (key: string, value: any) => {
+export const formatProfileValue = (
+ key: string,
+ value: any,
+ measurementSystem: MeasurementSystem = 'imperial'
+) => {
if (Array.isArray(value)) {
return value.join(', ')
}
@@ -456,7 +515,7 @@ export const formatProfileValue = (key: string, value: any) => {
case 'has_pets':
return value ? 'Yes' : 'No'
case 'height_in_inches':
- return `${Math.floor(value / 12)}' ${Math.round(value % 12)}"`
+ return formatHeight(value, measurementSystem)
case 'pref_age_max':
case 'pref_age_min':
return null // handle this in a special case
diff --git a/web/components/profile/profile-primary-info.tsx b/web/components/profile/profile-primary-info.tsx
index 4ac0ef99..e77d4b30 100644
--- a/web/components/profile/profile-primary-info.tsx
+++ b/web/components/profile/profile-primary-info.tsx
@@ -1,38 +1,55 @@
-import { ReactNode } from 'react'
-import { capitalize } from 'lodash'
-import { IoLocationOutline } from 'react-icons/io5'
-import { MdHeight } from 'react-icons/md'
-
-import { Row } from 'web/components/layout/row'
+import {ReactNode} from 'react'
+import {capitalize} from 'lodash'
+import {IoLocationOutline} from 'react-icons/io5'
+import {MdHeight} from 'react-icons/md'
+import {Row} from 'web/components/layout/row'
import GenderIcon from '../gender-icon'
-import { Gender, convertGender } from 'common/gender'
-import { formatProfileValue } from '../profile-about'
-import { Profile } from 'common/profiles/profile'
-import { useT } from 'web/lib/locale'
+import {convertGender, Gender} from 'common/gender'
+import {formatProfileValue} from '../profile-about'
+import {Profile} from 'common/profiles/profile'
+import {useT} from 'web/lib/locale'
+import {useMeasurementSystem} from 'web/hooks/use-measurement-system'
export default function ProfilePrimaryInfo(props: { profile: Profile }) {
- const { profile } = props
+ const {profile} = props
const t = useT()
+ const {measurementSystem} = useMeasurementSystem()
const stateOrCountry =
profile.country === 'United States of America'
? profile.region_code
: profile.country
return (
- {profile.city && }
- />}
- {profile.gender &&
- }
- />}
+ {profile.city && (
+ }
+ />
+ )}
+ {profile.gender && (
+
+ }
+ />
+ )}
{profile.height_in_inches != null && (
}
+ text={formatProfileValue(
+ 'height_in_inches',
+ profile.height_in_inches,
+ measurementSystem
+ )}
+ icon={}
/>
)}
@@ -40,7 +57,7 @@ export default function ProfilePrimaryInfo(props: { profile: Profile }) {
}
function IconWithInfo(props: { text: string; icon: ReactNode }) {
- const { text, icon } = props
+ const {text, icon} = props
return (
{icon}
diff --git a/web/hooks/use-measurement-system.ts b/web/hooks/use-measurement-system.ts
new file mode 100644
index 00000000..48677fe0
--- /dev/null
+++ b/web/hooks/use-measurement-system.ts
@@ -0,0 +1,39 @@
+import {usePersistentLocalState} from 'web/hooks/use-persistent-local-state'
+import {getLocale} from "web/lib/locale-cookie";
+
+export type MeasurementSystem = 'metric' | 'imperial'
+
+export const useMeasurementSystem = () => {
+ // Get default based on locale
+ const getDefaultMeasurementSystem = (): MeasurementSystem => {
+ if (typeof window !== 'undefined') {
+ try {
+ const saved = localStorage.getItem('measurement-system')
+ if (saved) {
+ const parsed = JSON.parse(saved)
+ if (parsed === 'metric' || parsed === 'imperial') {
+ return parsed
+ }
+ }
+ // Default based on locale
+ return getLocale() === 'en' ? 'imperial' : 'metric'
+ } catch (e) {
+ // Fallback to imperial if anything goes wrong
+ return 'imperial'
+ }
+ }
+ return 'imperial' // server-side default
+ }
+
+ const [measurementSystem, setMeasurementSystemState] =
+ usePersistentLocalState(
+ getDefaultMeasurementSystem(),
+ 'measurement-system'
+ )
+
+ const setMeasurementSystem = (newSystem: MeasurementSystem) => {
+ setMeasurementSystemState(newSystem)
+ }
+
+ return {measurementSystem, setMeasurementSystem}
+}
diff --git a/web/lib/measurement-utils.ts b/web/lib/measurement-utils.ts
new file mode 100644
index 00000000..a1429033
--- /dev/null
+++ b/web/lib/measurement-utils.ts
@@ -0,0 +1,69 @@
+import {MeasurementSystem} from 'web/hooks/use-measurement-system'
+
+// Conversion factors
+const INCHES_TO_CM = 2.54
+const MILES_TO_KM = 1.60934
+
+/**
+ * Format height in inches according to the specified measurement system
+ */
+export function formatHeight(
+ heightInInches: number,
+ measurementSystem: MeasurementSystem
+): string {
+ if (measurementSystem === 'metric') {
+ // Convert to centimeters
+ const cm = Math.round(heightInInches * INCHES_TO_CM)
+ return `${cm} cm`
+ } else {
+ // Show in feet and inches
+ const feet = Math.floor(heightInInches / 12)
+ const inches = Math.round(heightInInches % 12)
+ return `${feet}' ${inches}"`
+ }
+}
+
+/**
+ * Format distance in miles according to the specified measurement system
+ */
+export function formatDistance(
+ distanceInMiles: number,
+ measurementSystem: MeasurementSystem
+): string {
+ if (measurementSystem === 'metric') {
+ // Convert to kilometers
+ const km = Math.round(distanceInMiles * MILES_TO_KM)
+ return `${km} km`
+ } else {
+ // Show in miles
+ return `${distanceInMiles} miles`
+ }
+}
+
+/**
+ * Convert inches to centimeters
+ */
+export function inchesToCm(inches: number): number {
+ return inches * INCHES_TO_CM
+}
+
+/**
+ * Convert centimeters to inches
+ */
+export function cmToInches(cm: number): number {
+ return cm / INCHES_TO_CM
+}
+
+/**
+ * Convert miles to kilometers
+ */
+export function milesToKm(miles: number): number {
+ return miles * MILES_TO_KM
+}
+
+/**
+ * Convert kilometers to miles
+ */
+export function kmToMiles(km: number): number {
+ return km / MILES_TO_KM
+}
diff --git a/web/messages/de.json b/web/messages/de.json
index 20eb457d..f89834e2 100644
--- a/web/messages/de.json
+++ b/web/messages/de.json
@@ -1116,6 +1116,9 @@
"more_options_user.edit_bio": "Bio bearbeiten",
"vote.with_priority": "mit Priorität",
"settings.general.font": "Schriftart",
+ "settings.general.measurement": "Maßeinheitensystem",
+ "settings.measurement.imperial": "Imperial",
+ "settings.measurement.metric": "Metrisch",
"font.atkinson": "Atkinson Hyperlegible",
"font.system-sans": "System Sans",
"font.classic-serif": "Klassische Serifenschrift"
diff --git a/web/messages/fr.json b/web/messages/fr.json
index 631c8d10..9c745c5b 100644
--- a/web/messages/fr.json
+++ b/web/messages/fr.json
@@ -1116,6 +1116,9 @@
"more_options_user.edit_bio": "Options de biographie",
"vote.with_priority": "avec priorité",
"settings.general.font": "Police",
+ "settings.general.measurement": "Système de mesure",
+ "settings.measurement.imperial": "Impérial",
+ "settings.measurement.metric": "Métrique",
"font.atkinson": "Atkinson Hyperlegible",
"font.system-sans": "Sans-serif système",
"font.classic-serif": "Serif classique"
diff --git a/web/pages/settings.tsx b/web/pages/settings.tsx
index 89066223..e7f88584 100644
--- a/web/pages/settings.tsx
+++ b/web/pages/settings.tsx
@@ -8,25 +8,26 @@ import {NoSEO} from 'web/components/NoSEO'
import {UncontrolledTabs} from 'web/components/layout/tabs'
import {PageBase} from 'web/components/page-base'
import {Title} from 'web/components/widgets/title'
-import {useRedirectIfSignedOut} from "web/hooks/use-redirect-if-signed-out";
-import {deleteAccount} from "web/lib/util/delete";
-import router from "next/router";
-import {Button} from "web/components/buttons/button";
-import {updateEmail} from 'firebase/auth';
-import {NotificationSettings} from "web/components/notifications";
-import ThemeIcon from "web/components/theme-icon";
-import {WithPrivateUser} from "web/components/user/with-user";
-import {sendPasswordReset} from "web/lib/firebase/password";
-import {AboutSettings} from "web/components/about-settings";
-import {LanguagePicker} from "web/components/language/language-picker";
+import {useRedirectIfSignedOut} from 'web/hooks/use-redirect-if-signed-out'
+import {deleteAccount} from 'web/lib/util/delete'
+import router from 'next/router'
+import {Button} from 'web/components/buttons/button'
+import {updateEmail} from 'firebase/auth'
+import {NotificationSettings} from 'web/components/notifications'
+import ThemeIcon from 'web/components/theme-icon'
+import {WithPrivateUser} from 'web/components/user/with-user'
+import {sendPasswordReset} from 'web/lib/firebase/password'
+import {AboutSettings} from 'web/components/about-settings'
+import {LanguagePicker} from 'web/components/language/language-picker'
import {FontPicker} from 'web/components/font-picker'
-import {useT} from "web/lib/locale";
+import {useT} from 'web/lib/locale'
import HiddenProfilesModal from 'web/components/settings/hidden-profiles-modal'
-import {EmailVerificationButton} from "web/components/email-verification-button";
+import {EmailVerificationButton} from 'web/components/email-verification-button'
import {api} from 'web/lib/api'
-import {useUser} from "web/hooks/use-user";
-import {isNativeMobile} from "web/lib/util/webview";
-import {useFirebaseUser} from "web/hooks/use-firebase-user";
+import {useUser} from 'web/hooks/use-user'
+import {isNativeMobile} from 'web/lib/util/webview'
+import {useFirebaseUser} from 'web/hooks/use-firebase-user'
+import MeasurementSystemToggle from 'web/components/measurement-system-toggle'
export default function NotificationsPage() {
const t = useT()
@@ -38,9 +39,18 @@ export default function NotificationsPage() {
},
- {title: t('settings.tabs.notifications', 'Notifications'), content: },
- {title: t('settings.tabs.about', 'About'), content: },
+ {
+ title: t('settings.tabs.general', 'General'),
+ content: ,
+ },
+ {
+ title: t('settings.tabs.notifications', 'Notifications'),
+ content: ,
+ },
+ {
+ title: t('settings.tabs.about', 'About'),
+ content: ,
+ },
]}
trackingName={'settings page'}
/>
@@ -50,39 +60,50 @@ export default function NotificationsPage() {
export const GeneralSettings = () => (
- {user => }
+ {(user) => }
)
-const LoadedGeneralSettings = (props: {
- privateUser: PrivateUser,
-}) => {
+const LoadedGeneralSettings = (props: { privateUser: PrivateUser }) => {
const {privateUser} = props
const [isChangingEmail, setIsChangingEmail] = useState(false)
const [showHiddenProfiles, setShowHiddenProfiles] = useState(false)
- const {register, handleSubmit, formState: {errors}, reset} = useForm<{ newEmail: string }>()
+ const {
+ register,
+ handleSubmit,
+ formState: {errors},
+ reset,
+ } = useForm<{ newEmail: string }>()
const t = useT()
const user = useFirebaseUser()
if (!user) return null
const handleDeleteAccount = async () => {
- const confirmed = confirm(t('settings.delete_confirm', "Are you sure you want to delete your profile? This cannot be undone."))
+ const confirmed = confirm(
+ t(
+ 'settings.delete_confirm',
+ 'Are you sure you want to delete your profile? This cannot be undone.'
+ )
+ )
if (confirmed) {
toast
.promise(deleteAccount(), {
loading: t('settings.delete.loading', 'Deleting account...'),
success: () => {
router.push('/')
- return t('settings.delete.success', 'Your account has been deleted.')
+ return t(
+ 'settings.delete.success',
+ 'Your account has been deleted.'
+ )
},
error: () => {
return t('settings.delete.error', 'Failed to delete account.')
},
})
.catch(() => {
- console.log("Failed to delete account")
+ console.log('Failed to delete account')
})
}
}
@@ -92,115 +113,151 @@ const LoadedGeneralSettings = (props: {
try {
await updateEmail(user, newEmail)
- toast.success(t('settings.email.updated_success', 'Email updated successfully'))
+ toast.success(
+ t('settings.email.updated_success', 'Email updated successfully')
+ )
setIsChangingEmail(false)
reset()
// Force a reload to update the UI with the new email
// window.location.reload()
} catch (error: any) {
console.error('Error updating email:', error)
- toast.error(error.message || t('settings.email.update_failed', 'Failed to update email'))
+ toast.error(
+ error.message ||
+ t('settings.email.update_failed', 'Failed to update email')
+ )
}
}
const onSubmitEmailChange = (data: { newEmail: string }) => {
if (!user) return
if (data.newEmail === user.email) {
- toast.error(t('settings.email.same_as_current', 'New email is the same as current email'))
+ toast.error(
+ t(
+ 'settings.email.same_as_current',
+ 'New email is the same as current email'
+ )
+ )
return
}
changeUserEmail(data.newEmail)
}
- return <>
-
-
{t('settings.general.theme', 'Theme')}
-
+ return (
+ <>
+
-
{t('settings.general.language', 'Language')}
-
+
{t('settings.general.language', 'Language')}
+
-
{t('settings.general.font', 'Font')}
-
+
{t('settings.general.measurement', 'Measurement System')}
+
-
{t('settings.data_privacy.title', 'Data & Privacy')}
-
+
{t('settings.general.theme', 'Theme')}
+
-
{t('settings.general.people', 'People')}
- {/*
{t('settings.hidden_profiles.title', 'Hidden profiles')}
*/}
-
+
{t('settings.general.font', 'Font')}
+
-
{t('settings.general.account', 'Account')}
-
{t('settings.general.email', 'Email')}
+
{t('settings.data_privacy.title', 'Data & Privacy')}
+
-
-
- {!isChangingEmail ? (
-
+
- {/* Hidden profiles modal */}
-
- >
+ {!isChangingEmail ? (
+ setIsChangingEmail(true)}
+ className="w-fit"
+ >
+ {t('settings.email.change', 'Change email address')}
+
+ ) : (
+
+ )}
+
+ {t('settings.general.password', 'Password')}
+ sendPasswordReset(privateUser?.email)}
+ className="mb-2 max-w-[250px] w-fit"
+ color={'gray-outline'}
+ >
+ {t('settings.password.send_reset', 'Send password reset email')}
+
+
+ {t('settings.danger_zone', 'Danger Zone')}
+
+ {t('settings.delete_account', 'Delete Account')}
+
+
+
+ {/* Hidden profiles modal */}
+
+ >
+ )
}
const DataPrivacySettings = () => {
@@ -215,8 +272,12 @@ const DataPrivacySettings = () => {
setIsDownloading(true)
const data = await api('me/data', {})
const jsonString = JSON.stringify(data, null, 2)
- const filename = `compass-data-export${user?.username ? `-${user.username}` : ''}.json`;
- if (isNativeMobile() && window.AndroidBridge && window.AndroidBridge.downloadFile) {
+ const filename = `compass-data-export${user?.username ? `-${user.username}` : ''}.json`
+ if (
+ isNativeMobile() &&
+ window.AndroidBridge &&
+ window.AndroidBridge.downloadFile
+ ) {
window.AndroidBridge.downloadFile(filename, jsonString)
} else {
const blob = new Blob([jsonString], {type: 'application/json'})
@@ -256,11 +317,16 @@ const DataPrivacySettings = () => {
'Download a JSON file containing all your information: profile, account, messages, compatibility answers, starred profiles, votes, endorsements, search bookmarks, etc.'
)}
-
- {isDownloading ? t('settings.data_privacy.downloading', 'Downloading...')
- : t('settings.data_privacy.download', 'Download all my data (JSON)')
- }
+
+ {isDownloading
+ ? t('settings.data_privacy.downloading', 'Downloading...')
+ : t('settings.data_privacy.download', 'Download all my data (JSON)')}
)