mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-03-25 10:02:27 -04:00
273 lines
9.5 KiB
TypeScript
273 lines
9.5 KiB
TypeScript
import {PrivateUser} from 'common/src/user'
|
|
import {updateEmail} from 'firebase/auth'
|
|
import {useState} from 'react'
|
|
import {useForm} from 'react-hook-form'
|
|
import toast from 'react-hot-toast'
|
|
import {AboutSettings} from 'web/components/about-settings'
|
|
import {Button} from 'web/components/buttons/button'
|
|
import {ConnectionPreferencesSettings} from 'web/components/connection-preferences-settings'
|
|
import {EmailVerificationButton} from 'web/components/email-verification-button'
|
|
import {FontPicker} from 'web/components/font-picker'
|
|
import {LanguagePicker} from 'web/components/language/language-picker'
|
|
import {Col} from 'web/components/layout/col'
|
|
import {UncontrolledTabs} from 'web/components/layout/tabs'
|
|
import MeasurementSystemToggle from 'web/components/measurement-system-toggle'
|
|
import {NoSEO} from 'web/components/NoSEO'
|
|
import {NotificationSettings} from 'web/components/notifications'
|
|
import {PageBase} from 'web/components/page-base'
|
|
import {DeleteAccountSurveyModal} from 'web/components/profile/delete-account-survey-modal'
|
|
import HiddenProfilesModal from 'web/components/settings/hidden-profiles-modal'
|
|
import ThemeIcon from 'web/components/theme-icon'
|
|
import {WithPrivateUser} from 'web/components/user/with-user'
|
|
import {Input} from 'web/components/widgets/input'
|
|
import {Title} from 'web/components/widgets/title'
|
|
import {useFirebaseUser} from 'web/hooks/use-firebase-user'
|
|
import {useRedirectIfSignedOut} from 'web/hooks/use-redirect-if-signed-out'
|
|
import {useUser} from 'web/hooks/use-user'
|
|
import {api} from 'web/lib/api'
|
|
import {sendPasswordReset} from 'web/lib/firebase/password'
|
|
import {useT} from 'web/lib/locale'
|
|
import {isNativeMobile} from 'web/lib/util/webview'
|
|
|
|
export default function NotificationsPage() {
|
|
const t = useT()
|
|
useRedirectIfSignedOut()
|
|
return (
|
|
<PageBase trackPageView={'settings page'} className={'mx-4 mb-4'}>
|
|
<NoSEO />
|
|
<Title>{t('settings.title', 'Settings')}</Title>
|
|
<UncontrolledTabs
|
|
name={'settings-page'}
|
|
tabs={[
|
|
{
|
|
title: t('settings.tabs.general', 'General'),
|
|
content: <GeneralSettings />,
|
|
},
|
|
{
|
|
title: t('settings.tabs.notifications', 'Notifications'),
|
|
content: <NotificationSettings />,
|
|
},
|
|
{
|
|
title: t('settings.tabs.about', 'About'),
|
|
content: <AboutSettings />,
|
|
},
|
|
]}
|
|
trackingName={'settings page'}
|
|
/>
|
|
</PageBase>
|
|
)
|
|
}
|
|
|
|
export const GeneralSettings = () => (
|
|
<WithPrivateUser>{(user) => <LoadedGeneralSettings privateUser={user} />}</WithPrivateUser>
|
|
)
|
|
|
|
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 t = useT()
|
|
|
|
const firebaseUser = useFirebaseUser()
|
|
if (!firebaseUser) return null
|
|
|
|
const changeUserEmail = async (newEmail: string) => {
|
|
if (!firebaseUser) return
|
|
|
|
try {
|
|
await updateEmail(firebaseUser, newEmail)
|
|
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'))
|
|
}
|
|
}
|
|
|
|
const onSubmitEmailChange = (data: {newEmail: string}) => {
|
|
if (!firebaseUser) return
|
|
if (data.newEmail === firebaseUser.email) {
|
|
toast.error(t('settings.email.same_as_current', 'New email is the same as current email'))
|
|
return
|
|
}
|
|
changeUserEmail(data.newEmail)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col gap-2 max-w-fit">
|
|
<h3>{t('settings.general.language', 'Language')}</h3>
|
|
<LanguagePicker className={'w-fit min-w-[120px]'} />
|
|
|
|
<h3>{t('settings.general.measurement', 'Measurement System')}</h3>
|
|
<MeasurementSystemToggle />
|
|
|
|
<h3>{t('settings.general.theme', 'Theme')}</h3>
|
|
<ThemeIcon className="h-6 w-6" />
|
|
|
|
<h3>{t('settings.general.font', 'Font')}</h3>
|
|
<FontPicker className={'w-fit min-w-[180px]'} />
|
|
|
|
<h3>{t('settings.data_privacy.title', 'Data & Privacy')}</h3>
|
|
<DataPrivacySettings />
|
|
|
|
<h3>{t('settings.general.people', 'People')}</h3>
|
|
{/*<h5>{t('settings.hidden_profiles.title', 'Hidden profiles')}</h5>*/}
|
|
<Button
|
|
color={'gray-outline'}
|
|
onClick={() => setShowHiddenProfiles(true)}
|
|
className="w-fit"
|
|
>
|
|
{t('settings.hidden_profiles.manage', 'Manage hidden profiles')}
|
|
</Button>
|
|
|
|
<h3>{t('settings.connection_preferences.title', 'Connection Preferences')}</h3>
|
|
<ConnectionPreferencesSettings />
|
|
|
|
<h3>{t('settings.general.account', 'Account')}</h3>
|
|
<h5>{t('settings.general.email', 'Email')}</h5>
|
|
|
|
<EmailVerificationButton />
|
|
|
|
{!isChangingEmail ? (
|
|
<Button color={'gray-outline'} onClick={() => setIsChangingEmail(true)} className="w-fit">
|
|
{t('settings.email.change', 'Change email address')}
|
|
</Button>
|
|
) : (
|
|
<form onSubmit={handleSubmit(onSubmitEmailChange)} className="flex flex-col gap-2">
|
|
<Col>
|
|
<Input
|
|
type="email"
|
|
placeholder={t('settings.email.new_placeholder', 'New email address')}
|
|
{...register('newEmail', {
|
|
required: 'Email is required',
|
|
pattern: {
|
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
message: t('settings.email.invalid', 'Invalid email address'),
|
|
},
|
|
})}
|
|
disabled={!firebaseUser}
|
|
/>
|
|
{errors.newEmail && (
|
|
<span className="text-red-500 text-sm">
|
|
{errors.newEmail.message === 'Email is required'
|
|
? t('settings.email.required', 'Email is required')
|
|
: errors.newEmail.message}
|
|
</span>
|
|
)}
|
|
</Col>
|
|
<div className="flex gap-2">
|
|
<Button type="submit" color="green" className="w-fit">
|
|
{t('settings.action.save', 'Save')}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="gray"
|
|
onClick={() => {
|
|
setIsChangingEmail(false)
|
|
reset()
|
|
}}
|
|
className="w-fit"
|
|
>
|
|
{t('settings.action.cancel', 'Cancel')}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
<h5>{t('settings.general.password', 'Password')}</h5>
|
|
<Button
|
|
onClick={() => sendPasswordReset(privateUser?.email)}
|
|
className="mb-2 max-w-[250px] w-fit"
|
|
color={'gray-outline'}
|
|
>
|
|
{t('settings.password.send_reset', 'Send password reset email')}
|
|
</Button>
|
|
|
|
<h5>{t('settings.danger_zone', 'Danger Zone')}</h5>
|
|
<div className={'w-fit'}>
|
|
<DeleteAccountSurveyModal />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hidden profiles modal */}
|
|
<HiddenProfilesModal open={showHiddenProfiles} setOpen={setShowHiddenProfiles} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const DataPrivacySettings = () => {
|
|
const t = useT()
|
|
const user = useUser()
|
|
const [isDownloading, setIsDownloading] = useState(false)
|
|
|
|
const handleDownload = async () => {
|
|
if (isDownloading) return
|
|
|
|
try {
|
|
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) {
|
|
window.AndroidBridge.downloadFile(filename, jsonString)
|
|
} else {
|
|
const blob = new Blob([jsonString], {type: 'application/json'})
|
|
const url = URL.createObjectURL(blob)
|
|
const link = document.createElement('a')
|
|
link.href = url
|
|
link.download = filename
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
toast.success(
|
|
t('settings.data_privacy.download.success', 'Your data export has been downloaded.'),
|
|
)
|
|
} catch (e) {
|
|
console.error('Error downloading data export', e)
|
|
toast.error(
|
|
t(
|
|
'settings.data_privacy.download.error',
|
|
'Failed to download your data. Please try again.',
|
|
),
|
|
)
|
|
} finally {
|
|
setIsDownloading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4 max-w-xl">
|
|
<p className="text-sm guidance">
|
|
{t(
|
|
'settings.data_privacy.description',
|
|
'Download a JSON file containing all your information: profile, account, messages, compatibility answers, starred profiles, votes, endorsements, search bookmarks, etc.',
|
|
)}
|
|
</p>
|
|
<Button
|
|
color="gray-outline"
|
|
onClick={handleDownload}
|
|
className="w-fit"
|
|
disabled={isDownloading}
|
|
loading={isDownloading}
|
|
>
|
|
{isDownloading
|
|
? t('settings.data_privacy.downloading', 'Downloading...')
|
|
: t('settings.data_privacy.download', 'Download all my data (JSON)')}
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|