import { UploadFile, createFileUploader } from '@solid-primitives/upload' import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { For, Match, Show, Switch, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { useLocalize } from '~/context/localize' import { useProfile } from '~/context/profile' import { useSession } from '~/context/session' import { useSnackbar, useUI } from '~/context/ui' import { InputMaybe, ProfileInput } from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getThumbUrl' import { handleImageUpload } from '~/lib/handleImageUpload' import { clone } from '~/utils/clone' import { validateUrl } from '~/utils/validate' import { ProfileSettingsNavigation } from '../../ProfileNav' import { Button } from '../../_shared/Button' import { Icon } from '../../_shared/Icon' import { ImageCropper } from '../../_shared/ImageCropper' import { Loading } from '../../_shared/Loading' import { Modal } from '../../_shared/Modal' import { Popover } from '../../_shared/Popover' import { SocialNetworkInput } from '../../_shared/SocialNetworkInput' import styles from './Settings.module.scss' import { profileSocialLinks } from './profileSocialLinks' const SimplifiedEditor = lazy(() => import('~/components/Editor/SimplifiedEditor')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea')) function filterNulls(arr: InputMaybe[]): string[] { return arr.filter((item): item is string => item !== null && item !== undefined) } export const ProfileSettings = () => { const { t } = useLocalize() const [isFormInitialized, setIsFormInitialized] = createSignal(false) const [isSaving, setIsSaving] = createSignal(false) const [social, setSocial] = createSignal([]) const [addLinkForm, setAddLinkForm] = createSignal(false) const [incorrectUrl, setIncorrectUrl] = createSignal(false) const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false) const [userpicFile, setUserpicFile] = createSignal() const [uploadError, setUploadError] = createSignal(false) const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) const [hostname, setHostname] = createSignal(null) const [slugError, setSlugError] = createSignal() const [nameError, setNameError] = createSignal() const { form, submit, updateFormField, setForm } = useProfile() const { showSnackbar } = useSnackbar() const { loadSession, session } = useSession() const [prevForm, setPrevForm] = createStore() const { showConfirm } = useUI() const [clearAbout, setClearAbout] = createSignal(false) const { showModal, hideModal } = useUI() const [loading, setLoading] = createSignal(true) // Используем createEffect для отслеживания данных сессии и инициализации формы createEffect(() => { const s = session() if (s && !isFormInitialized()) { const profileData = s?.user?.app_data?.profile if (profileData) { setPrevForm(profileData) const soc: string[] = filterNulls(profileData.links || []) setSocial(soc) setForm(profileData) // Инициализируем форму с данными профиля setIsFormInitialized(true) } setLoading(false) // Отключаем загрузку только после инициализации данных } }) let slugInputRef: HTMLInputElement | null let nameInputRef: HTMLInputElement | null const handleChangeSocial = (value: string) => { if (validateUrl(value)) { updateFormField('links', value) setAddLinkForm(false) } else { setIncorrectUrl(true) } } const handleSubmit = async (event: MouseEvent | undefined) => { event?.preventDefault() setIsSaving(true) if (nameInputRef?.value.length === 0) { setNameError(t('Required')) nameInputRef?.focus() setIsSaving(false) return } if (slugInputRef?.value.length === 0) { setSlugError(t('Required')) slugInputRef?.focus() setIsSaving(false) return } try { await submit(form) setPrevForm(clone(form)) showSnackbar({ body: t('Profile successfully saved') }) } catch (error) { if (error?.toString().search('duplicate_slug')) { setSlugError(t('The address is already taken')) slugInputRef?.focus() return } showSnackbar({ type: 'error', body: t('Error') }) } finally { setIsSaving(false) } setTimeout(loadSession, 5000) // renews author's profile } const handleCancel = async () => { const isConfirmed = await showConfirm({ confirmBody: t('Do you really want to reset all changes?'), confirmButtonVariant: 'primary', declineButtonVariant: 'secondary' }) if (isConfirmed) { setClearAbout(true) setForm(clone(prevForm)) setClearAbout(false) } } const handleCropAvatar = () => { const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) selectFiles(([uploadFile]) => { setUserpicFile(uploadFile as UploadFile) showModal('cropImage') }) } const handleUploadAvatar = async (uploadFile: UploadFile) => { try { setUploadError(false) setIsUserpicUpdating(true) const result = await handleImageUpload(uploadFile, session()?.access_token || '') updateFormField('pic', result.url) setUserpicFile(undefined) setIsUserpicUpdating(false) } catch (error) { setUploadError(true) console.error('[upload avatar] error', error) } } onMount(() => { setHostname(window?.location.host) // eslint-disable-next-line unicorn/consistent-function-scoping const handleBeforeUnload = (event: BeforeUnloadEvent) => { if (!deepEqual(form, prevForm)) { event.returnValue = t( 'There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?' ) } } window.addEventListener('beforeunload', handleBeforeUnload) onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload)) }) createEffect( on( () => deepEqual(form, prevForm), () => { if (Object.keys(prevForm).length > 0) { setIsFloatingPanelVisible(!deepEqual(form, prevForm)) } } ) ) const handleDeleteSocialLink = (link: string) => { updateFormField('links', link, true) } const slugUpdate = (ev: InputEvent) => { const input = (ev.target || ev.currentTarget) as HTMLInputElement const value = input.value const newValue = value.startsWith('@') || value.startsWith('!') ? value.substring(1) : value input.value = newValue updateFormField('slug', newValue) } return ( 0 && isFormInitialized() && !loading()} fallback={}> <>

{t('Profile settings')}

{t('Here you can customize your profile the way you want.')}

{t('Userpic')}

{(triggerRef: (el: HTMLElement) => void) => ( )} {/* @@TODO inspect popover below. onClick causes page refreshing */} {/* {(triggerRef: (el: HTMLElement) => void) => ( )} */}
{t('Here you can upload your photo')}
{t('Upload error')}

{t('Name')}

{t( 'Your name will appear on your profile page and as your signature in publications, comments and responses.' )}

updateFormField('name', event.currentTarget.value)} value={form.name || ''} ref={(el) => (nameInputRef = el)} />
{t(`${nameError()}`)}

{t('Address on Discours')}

(slugInputRef = el)} class="nolabel" />

{t(`${slugError()}`)}

{t('Introduce')}

updateFormField('bio', value)} initialValue={form.bio || ''} allowEnterKey={false} maxLength={120} />

{t('About')}

updateFormField('about', value)} />

{t('Social networks')}

handleChangeSocial(value)} />

{t('It does not look like url')}

{(network) => ( handleChangeSocial(value)} isExist={!network.isPlaceholder} slug={form.slug || ''} handleDelete={() => handleDeleteSocialLink(network.link)} /> )}
setUserpicFile(undefined)}>

{t('Crop image')}

{ handleUploadAvatar(data) hideModal() }} onDecline={() => hideModal()} />
) }