Cache data for required form as well

This commit is contained in:
MartinBraquet
2026-02-14 01:50:46 +01:00
parent ccf68b80c0
commit d61133ef74
7 changed files with 135 additions and 80 deletions

View File

@@ -8,8 +8,8 @@ android {
applicationId "com.compassconnections.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 36
versionName "1.6.0"
versionCode 38
versionName "1.7.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

@@ -15,6 +15,7 @@ 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'
import {useRouter} from "next/router";
export function BioTips({onClick}: { onClick?: () => void }) {
const t = useT();
@@ -27,6 +28,13 @@ export function BioTips({onClick}: { onClick?: () => void }) {
- Optional: romantic preferences, lifestyle habits, and conversation starters
`);
let href = '/tips-bio'
const router = useRouter()
if (router.pathname === '/signup') {
href += '?fromSignup=true'
}
return (
<ShowMore
labelClosed={t('profile.bio.tips', 'Tips')}
@@ -36,7 +44,7 @@ export function BioTips({onClick}: { onClick?: () => void }) {
<p>{t('profile.bio.tips_intro', "Write a clear and engaging bio to help others understand who you are and the connections you seek. Include:")}</p>
<ReactMarkdown>{tips}</ReactMarkdown>
<NewTabLink
href="/tips-bio"
href={href}
onClick={onClick}>{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}</NewTabLink>
</ShowMore>
)

View File

@@ -28,7 +28,7 @@ export function EmailVerificationButton() {
}
return (
<Col className={'gap-2 mx-4'}>
<Col className={'gap-2'}>
<Button
color={'gray-outline'}
onClick={() => sendVerificationEmail(user, t)}

View File

@@ -5,6 +5,7 @@ import {SEO} from "web/components/SEO";
import {capitalize} from "lodash";
import {CustomLink} from "web/components/links";
import {BackButton} from "web/components/back-button";
import {useRouter} from "next/router";
export const MD_PATHS = [
'constitution',
@@ -32,6 +33,25 @@ export const CustomMarkdown = ({children}: { children: string }) => {
export default function MarkdownPage({content, filename}: Props) {
const title = /[A-Z]/.test(filename) ? filename : capitalize(filename)
const router = useRouter()
const {query} = router
const fromSignup = query.fromSignup === 'true'
if (fromSignup) {
return (
<Col className="items-center">
<Col className="items-center justify-center mb-8 max-w-4xl">
<Col className='w-full rounded px-3 py-4 sm:px-6 space-y-4'>
<BackButton className="-ml-2 self-start"/>
<div className={'custom-link !mt-0'}>
<CustomMarkdown>{content}</CustomMarkdown>
</div>
</Col>
</Col>
</Col>
)
}
return (
<PageBase trackPageView={filename} className={'col-span-8'}>
<SEO
@@ -41,7 +61,6 @@ export default function MarkdownPage({content, filename}: Props) {
/>
<Col className="items-center mb-8">
<Col className='w-full rounded px-3 py-4 sm:px-6 space-y-4'>
<BackButton className="-ml-2 self-start"/>
<div className={'custom-link !mt-0'}>
<CustomMarkdown>{content}</CustomMarkdown>
</div>

View File

@@ -1,4 +1,4 @@
import {Fragment, useCallback, useEffect, useRef, useState} from 'react'
import {Fragment, useEffect, useRef, useState} from 'react'
import {Title} from 'web/components/widgets/title'
import {Col} from 'web/components/layout/col'
import clsx from 'clsx'
@@ -27,6 +27,7 @@ import {City, CityRow, profileToCity, useCitySearch} from "web/components/search
import {AddPhotosWidget} from './widgets/add-photos'
import {RadioToggleGroup} from "web/components/widgets/radio-toggle-group";
import {MultipleChoiceOptions} from "common/profiles/multiple-choice";
import {useProfileDraft} from 'web/hooks/use-profile-draft'
import {
DIET_CHOICES,
EDUCATION_CHOICES,
@@ -48,7 +49,6 @@ 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: {
@@ -100,79 +100,7 @@ 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
const {clearProfileDraft} = useProfileDraft(user.id, profile, setProfile, updateHeight)
useEffect(() => {
fetchChoices('interests', locale).then(setInterestChoices)

View File

@@ -13,6 +13,7 @@ import {ProfileRow, ProfileWithoutUser} from 'common/profiles/profile'
import {SignupBio} from "web/components/bio/editable-bio"
import {Editor} from "@tiptap/core"
import {useT} from 'web/lib/locale'
import {useProfileDraft} from 'web/hooks/use-profile-draft'
export const initialRequiredState = {
age: undefined,
@@ -53,6 +54,14 @@ export const RequiredProfileUserForm = (props: {
const [step, setStep] = useState<number>(0)
const t = useT()
const {draftLoaded} = useProfileDraft(user.id, profile, setProfile)
useEffect(() => {
if (draftLoaded) {
setStep(1)
}
}, [draftLoaded])
const {
name,
username,
@@ -140,6 +149,7 @@ export const RequiredProfileUserForm = (props: {
{t('profile.basics.bio', 'Bio')}
</label>
<SignupBio
profile={profile}
onChange={(e: Editor) => {
console.debug('bio changed', e, profile.bio)
setProfile('bio', e.getJSON())

View File

@@ -0,0 +1,90 @@
import {useCallback, useEffect, useState} from 'react'
import {ProfileWithoutUser} from 'common/profiles/profile'
const safeLocalStorage = typeof window !== 'undefined' ? window.localStorage : null
export const useProfileDraft = (
userId: string,
profile: ProfileWithoutUser,
setProfile: <K extends keyof ProfileWithoutUser>(key: K, value: ProfileWithoutUser[K]) => void,
updateHeight?: (value: number | undefined) => void
) => {
const KEY = `draft-profile-${userId}`
const [draftLoaded, setDraftLoaded] = useState(false)
const clearProfileDraft = (userId: string) => {
try {
safeLocalStorage?.removeItem(`draft-profile-${userId}`)
} 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, userId, 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) {
updateHeight(value as number)
}
}
})
setDraftLoaded(true)
}
}
} catch (error) {
console.warn('Failed to load profile from store:', error)
}
}, []) // Only run once on mount
return {clearProfileDraft, draftLoaded}
}