mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-19 15:27:16 -05:00
Cache unsaved profile editions for 24h
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
<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">{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}</NewTabLink>
|
||||
href="/tips-bio"
|
||||
onClick={onClick}>{t('profile.bio.tips_link', 'Read full tips for writing a high-quality bio')}</NewTabLink>
|
||||
</ShowMore>
|
||||
)
|
||||
}
|
||||
@@ -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<any>(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 (
|
||||
<Col className="relative w-full">
|
||||
<BaseBio
|
||||
@@ -111,6 +129,11 @@ export function SignupBio(props: {
|
||||
if (!editor) return
|
||||
onChange(editor)
|
||||
}}
|
||||
onClickTips={onClickTips}
|
||||
onEditor={(e) => {
|
||||
setEditor(e)
|
||||
onEditor?.(e)
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
@@ -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) {
|
||||
}
|
||||
</p>
|
||||
}
|
||||
<BioTips/>
|
||||
<BioTips onClick={onClickTips}/>
|
||||
<TextEditor
|
||||
editor={editor}
|
||||
onBlur={() => onBlur?.(editor)}
|
||||
|
||||
@@ -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<Record<string, string | null>>(
|
||||
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}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
@@ -325,6 +415,7 @@ export const OptionalProfileUserForm = (props: {
|
||||
}}
|
||||
className={'w-16'}
|
||||
value={typeof heightInches === 'number' ? Math.floor(heightInches) : ''}
|
||||
min={0}
|
||||
/>
|
||||
</Col>
|
||||
<div className="self-end mb-2 text-ink-700 mx-2">
|
||||
@@ -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}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<Link href={href} target={isNative ? undefined : "_blank"} className={className}>
|
||||
<Link href={href} onClick={onClick} target={isNative ? undefined : "_blank"} className={className}>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user