diff --git a/backend/supabase/profiles.sql b/backend/supabase/profiles.sql
index 5cde564..4cc4f82 100644
--- a/backend/supabase/profiles.sql
+++ b/backend/supabase/profiles.sql
@@ -25,7 +25,7 @@ CREATE TABLE IF NOT EXISTS profiles (
gender TEXT NOT NULL,
geodb_city_id TEXT,
has_kids INTEGER,
- height_in_inches INTEGER,
+ height_in_inches float4,
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
image_descriptions jsonb,
is_smoker BOOLEAN,
diff --git a/web/components/bio/editable-bio.tsx b/web/components/bio/editable-bio.tsx
index 1bee0a8..262b899 100644
--- a/web/components/bio/editable-bio.tsx
+++ b/web/components/bio/editable-bio.tsx
@@ -14,8 +14,9 @@ import {MIN_BIO_LENGTH} from "common/constants";
import {ShowMore} from 'web/components/widgets/show-more'
import {NewTabLink} from 'web/components/widgets/new-tab-link'
import {useT} from 'web/lib/locale'
+import {richTextToString} from 'common/util/parse'
-export function BioTips() {
+export function BioTips({onClick}: { onClick?: () => void }) {
const t = useT();
const tips = t('profile.bio.tips_list', `
- Your core values, interests, and activities
@@ -35,7 +36,8 @@ export function BioTips() {
{t('profile.bio.tips_intro', "Write a clear and engaging bio to help others understand who you are and the connections you seek. Include:")}
{tips}
{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}
+ href="/tips-bio"
+ onClick={onClick}>{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}
)
}
@@ -71,6 +73,7 @@ export function EditableBio(props: {
defaultValue={profile.bio}
onEditor={(e) => {
setEditor(e);
+ if (e) setTextLength(e.getText().length)
e?.on('update', () => {
setTextLength(e.getText().length);
});
@@ -101,8 +104,23 @@ export function EditableBio(props: {
export function SignupBio(props: {
onChange: (e: Editor) => void
profile?: ProfileWithoutUser | undefined
+ onClickTips?: () => void
+ onEditor?: (editor: any) => void
}) {
- const {onChange, profile} = props
+ const {onChange, profile, onClickTips, onEditor} = props
+ const [editor, setEditor] = useState(null)
+
+ // Keep the editor content in sync with profile.bio when it becomes available
+ useEffect(() => {
+ if (!editor) return
+ const profileText = profile?.bio ? richTextToString(profile.bio as any) : ''
+ const currentText = editor?.getText?.() ?? ''
+ // Only update if the underlying text differs to avoid clobbering user input unnecessarily
+ if (profileText !== currentText) {
+ editor.commands.setContent(profile?.bio ?? '')
+ }
+ }, [profile?.bio, editor])
+
return (
{
+ setEditor(e)
+ onEditor?.(e)
+ }}
/>
)
@@ -120,9 +143,10 @@ interface BaseBioProps {
defaultValue?: any
onBlur?: (editor: any) => void
onEditor?: (editor: any) => void
+ onClickTips?: () => void
}
-export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
+export function BaseBio({defaultValue, onBlur, onEditor, onClickTips}: BaseBioProps) {
const t = useT();
const editor = useTextEditor({
// extensions: [StarterKit],
@@ -147,7 +171,7 @@ export function BaseBio({defaultValue, onBlur, onEditor}: BaseBioProps) {
}
}
-
+
onBlur?.(editor)}
diff --git a/web/components/optional-profile-form.tsx b/web/components/optional-profile-form.tsx
index 83205af..2437250 100644
--- a/web/components/optional-profile-form.tsx
+++ b/web/components/optional-profile-form.tsx
@@ -1,4 +1,4 @@
-import {Fragment, useEffect, useRef, useState} from 'react'
+import {Fragment, useCallback, useEffect, useRef, useState} from 'react'
import {Title} from 'web/components/widgets/title'
import {Col} from 'web/components/layout/col'
import clsx from 'clsx'
@@ -48,6 +48,7 @@ import {sleep} from "common/util/time"
import {SignupBio} from "web/components/bio/editable-bio";
import {Editor} from "@tiptap/core";
import {Slider} from "web/components/widgets/slider";
+import {safeLocalStorage} from "web/lib/util/local";
export const OptionalProfileUserForm = (props: {
@@ -76,6 +77,18 @@ export const OptionalProfileUserForm = (props: {
: undefined
)
+ // Keep local feet/inches inputs in sync when profile.height_in_inches updates
+ // This covers cases like hydration from localStorage where setProfile is called externally
+ const updateHeight = (h: any) => {
+ if (h == null || Number.isNaN(h as any)) {
+ setHeightFeet(undefined)
+ setHeightInches(undefined)
+ return
+ }
+ setHeightFeet(Math.floor(h / 12))
+ setHeightInches(Math.round(h % 12))
+ }
+
const [newLinks, setNewLinks] = useState>(
user.link
)
@@ -87,6 +100,80 @@ export const OptionalProfileUserForm = (props: {
const [workChoices, setWorkChoices] = useState({})
const {locale} = useLocale()
+ const KEY = `draft-profile-${user.id}`
+
+ const clearProfileDraft = (userId: string) => {
+ try {
+ safeLocalStorage?.removeItem(`draft-profile-${userId}`)
+ safeLocalStorage?.removeItem(`draft-profile-${userId}-timestamp`)
+ } catch (error) {
+ console.warn('Failed to clear profile from store:', error)
+ }
+ }
+
+ // Debounced save function
+ const debouncedSaveProfile = useCallback(
+ (() => {
+ let timeoutId: NodeJS.Timeout
+ return (profileToSave: ProfileWithoutUser) => {
+ clearTimeout(timeoutId)
+ timeoutId = setTimeout(() => {
+ try {
+ safeLocalStorage?.setItem(KEY, JSON.stringify({
+ profile: profileToSave,
+ timestamp: Date.now().toString(),
+ }))
+ } catch (error) {
+ console.warn('Failed to save profile to store:', error)
+ }
+ }, 500) // 500ms debounce delay
+ }
+ })(),
+ [KEY]
+ )
+
+ useEffect(() => {
+ console.log({profile})
+ if (profile && Object.keys(profile).length > 0) {
+ debouncedSaveProfile(profile)
+ }
+ }, [profile, user.id, debouncedSaveProfile])
+
+ useEffect(() => {
+ try {
+ const savedProfileString = safeLocalStorage?.getItem(KEY)
+ if (savedProfileString) {
+ const data = JSON.parse(savedProfileString)
+ if (data) {
+ const {profile: savedProfile, timestamp} = data
+ // Check if saved data is older than 24 hours
+ if (timestamp) {
+ const savedTime = parseInt(timestamp, 10)
+ const now = Date.now()
+ const twentyFourHoursInMs = 24 * 60 * 60 * 1000
+
+ if (now - savedTime > twentyFourHoursInMs) {
+ console.log('Skipping profile update: saved data is older than 24 hours')
+ return
+ }
+ }
+
+ // Update all profile fields
+ Object.entries(savedProfile).forEach(([key, value]) => {
+ const typedKey = key as keyof ProfileWithoutUser
+ if (value !== profile[typedKey]) {
+ console.log(key, value)
+ setProfile(typedKey, value)
+ if (typedKey === 'height_in_inches') updateHeight(value)
+ }
+ })
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load profile from store:', error)
+ }
+ }, []) // Only run once on mount
+
useEffect(() => {
fetchChoices('interests', locale).then(setInterestChoices)
fetchChoices('causes', locale).then(setCauseChoices)
@@ -120,6 +207,8 @@ export const OptionalProfileUserForm = (props: {
}
try {
await Promise.all(promises)
+ // Clear profile draft from Zustand store after successful submission
+ clearProfileDraft(user.id)
} catch (error) {
console.error(error)
toast.error(
@@ -308,6 +397,7 @@ export const OptionalProfileUserForm = (props: {
}}
className={'w-16'}
value={typeof heightFeet === 'number' ? Math.floor(heightFeet) : ''}
+ min={0}
/>
@@ -325,6 +415,7 @@ export const OptionalProfileUserForm = (props: {
}}
className={'w-16'}
value={typeof heightInches === 'number' ? Math.floor(heightInches) : ''}
+ min={0}
/>
@@ -348,9 +439,10 @@ export const OptionalProfileUserForm = (props: {
}
}}
className={'w-20'}
- value={heightFeet !== undefined
- ? Math.round((heightFeet * 12 + (heightInches ?? 0)) * 2.54)
+ value={heightFeet !== undefined && profile['height_in_inches']
+ ? Math.round(profile['height_in_inches'] * 2.54)
: ''}
+ min={0}
/>
diff --git a/web/components/profile-about.tsx b/web/components/profile-about.tsx
index aa9eefe..8dad4c6 100644
--- a/web/components/profile-about.tsx
+++ b/web/components/profile-about.tsx
@@ -456,7 +456,7 @@ export const formatProfileValue = (key: string, value: any) => {
case 'has_pets':
return value ? 'Yes' : 'No'
case 'height_in_inches':
- return `${Math.floor(value / 12)}' ${value % 12}"`
+ return `${Math.floor(value / 12)}' ${Math.round(value % 12)}"`
case 'pref_age_max':
case 'pref_age_min':
return null // handle this in a special case
diff --git a/web/components/widgets/new-tab-link.tsx b/web/components/widgets/new-tab-link.tsx
index 9d04624..941c828 100644
--- a/web/components/widgets/new-tab-link.tsx
+++ b/web/components/widgets/new-tab-link.tsx
@@ -5,13 +5,14 @@ interface NewTabLinkProps {
href: string
children: React.ReactNode
className?: string
+ onClick?: () => void
}
-export function NewTabLink({href, children, className}: NewTabLinkProps) {
+export function NewTabLink({href, children, className, onClick}: NewTabLinkProps) {
// New tabs don't work on native apps
const isNative = isNativeMobile()
return (
-
+
{children}
)