Fix/profile update (#327)

Profile settings refactoring
This commit is contained in:
Ilya Y 2023-11-23 21:15:06 +03:00 committed by GitHub
parent 57a289e383
commit 954d964384
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 450 additions and 355 deletions

View File

@ -103,6 +103,7 @@
"Discussion rules": "Discussion rules",
"Discussion rules in social networks": "Discussion rules",
"Discussions": "Discussions",
"Do you really want to reset all changes?": "Do you really want to reset all changes?",
"Dogma": "Dogma",
"Draft successfully deleted": "Draft successfully deleted",
"Drafts": "Drafts",
@ -329,6 +330,7 @@
"Terms of use": "Site rules",
"Text checking": "Text checking",
"Thank you": "Thank you",
"The address is already taken": "The address is already taken",
"Theory": "Theory",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?",

View File

@ -106,6 +106,7 @@
"Discussion rules": "Правила дискуссий",
"Discussion rules in social networks": "Правила сообществ самиздата в соцсетях",
"Discussions": "Дискуссии",
"Do you really want to reset all changes?": "Вы действительно хотите сбросить все изменения?",
"Dogma": "Догма",
"Draft successfully deleted": "Черновик успешно удален",
"Drafts": "Черновики",
@ -347,6 +348,7 @@
"Terms of use": "Правила сайта",
"Text checking": "Проверка текста",
"Thank you": "Благодарности",
"The address is already taken": "Адрес уже занят",
"Theory": "Теории",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",

View File

@ -0,0 +1,359 @@
import { createFileUploader } from '@solid-primitives/upload'
import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal'
import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js'
import { createStore } from 'solid-js/store'
import { useConfirm } from '../../context/confirm'
import { useLocalize } from '../../context/localize'
import { useProfileForm } from '../../context/profile'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { clone } from '../../utils/clone'
import { getImageUrl } from '../../utils/getImageUrl'
import { handleImageUpload } from '../../utils/handleImageUpload'
import { profileSocialLinks } from '../../utils/profileSocialLinks'
import { validateUrl } from '../../utils/validateUrl'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Loading } from '../_shared/Loading'
import { Popover } from '../_shared/Popover'
import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation'
import styles from '../../pages/profile/Settings.module.scss'
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea'))
export const ProfileSettings = () => {
const { t } = useLocalize()
const [prevForm, setPrevForm] = createStore({})
const [isFormInitialized, setIsFormInitialized] = createSignal(false)
const [social, setSocial] = createSignal([])
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
const [uploadError, setUploadError] = createSignal(false)
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
const [hostname, setHostname] = createSignal<string | null>(null)
const [slugError, setSlugError] = createSignal<string>()
const [nameError, setNameError] = createSignal<string>()
const {
form,
actions: { submit, updateFormField, setForm },
} = useProfileForm()
const {
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { loadSession },
} = useSession()
const {
actions: { showConfirm },
} = useConfirm()
createEffect(() => {
if (Object.keys(form).length > 0 && !isFormInitialized()) {
setPrevForm(form)
setSocial(form.links)
setIsFormInitialized(true)
}
})
const slugInputRef: { current: HTMLInputElement } = { current: null }
const nameInputRef: { current: HTMLInputElement } = { current: null }
const handleChangeSocial = (value: string) => {
if (validateUrl(value)) {
updateFormField('links', value)
setAddLinkForm(false)
} else {
setIncorrectUrl(true)
}
}
const handleSubmit = async (event: Event) => {
event.preventDefault()
if (nameInputRef.current.value.length === 0) {
setNameError(t('Required'))
nameInputRef.current.focus()
return
}
if (slugInputRef.current.value.length === 0) {
setSlugError(t('Required'))
slugInputRef.current.focus()
return
}
try {
await submit(form)
setPrevForm(clone(form))
showSnackbar({ body: t('Profile successfully saved') })
} catch (error) {
if (error.code === 'duplicate_slug') {
setSlugError(t('The address is already taken'))
slugInputRef.current.focus()
return
}
showSnackbar({ type: 'error', body: t('Error') })
}
loadSession()
}
const handleCancel = async () => {
const isConfirmed = await showConfirm({
confirmBody: t('Do you really want to reset all changes?'),
confirmButtonVariant: 'primary',
declineButtonVariant: 'secondary',
})
if (isConfirmed) {
setForm(clone(prevForm))
}
}
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const handleUploadAvatar = async () => {
selectFiles(async ([uploadFile]) => {
try {
setUploadError(false)
setIsUserpicUpdating(true)
const result = await handleImageUpload(uploadFile)
updateFormField('userpic', result.url)
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) => {
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(() => {
if (!deepEqual(form, prevForm)) {
setIsFloatingPanelVisible(true)
}
})
const handleDeleteSocialLink = (link) => {
updateFormField('links', link, true)
}
return (
<Show when={Object.keys(form).length > 0 && isFormInitialized()} fallback={<Loading />}>
<>
<div class="wide-container">
<div class="row">
<div class="col-md-5">
<div class={clsx('left-navigation', styles.leftNavigation)}>
<ProfileSettingsNavigation />
</div>
</div>
<div class="col-md-19">
<div class="row">
<div class="col-md-20 col-lg-18 col-xl-16">
<h1>{t('Profile settings')}</h1>
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
<form enctype="multipart/form-data">
<h4>{t('Userpic')}</h4>
<div class="pretty-form__item">
<div
class={clsx(styles.userpic, { [styles.hasControls]: form.userpic })}
onClick={!form.userpic && handleUploadAvatar}
>
<Switch>
<Match when={isUserpicUpdating()}>
<Loading />
</Match>
<Match when={form.userpic}>
<div
class={styles.userpicImage}
style={{
'background-image': `url(${getImageUrl(form.userpic, {
width: 180,
height: 180,
})})`,
}}
/>
<div class={styles.controls}>
<Popover content={t('Delete userpic')}>
{(triggerRef: (el) => void) => (
<button
ref={triggerRef}
class={styles.control}
onClick={() => updateFormField('userpic', '')}
>
<Icon name="close" />
</button>
)}
</Popover>
<Popover content={t('Upload userpic')}>
{(triggerRef: (el) => void) => (
<button
ref={triggerRef}
class={styles.control}
onClick={handleUploadAvatar}
>
<Icon name="user-image-black" />
</button>
)}
</Popover>
</div>
</Match>
<Match when={!form.userpic}>
<Icon name="user-image-gray" />
{t('Here you can upload your photo')}
</Match>
</Switch>
</div>
<Show when={uploadError()}>
<div class={styles.error}>{t('Upload error')}</div>
</Show>
</div>
<h4>{t('Name')}</h4>
<p class="description">
{t(
'Your name will appear on your profile page and as your signature in publications, comments and responses.',
)}
</p>
<div class="pretty-form__item">
<input
type="text"
name="username"
id="username"
placeholder={t('Name')}
onInput={(event) => updateFormField('name', event.currentTarget.value)}
value={form.name}
ref={(el) => (nameInputRef.current = el)}
/>
<label for="username">{t('Name')}</label>
<Show when={nameError()}>
<div
style={{ position: 'absolute', 'margin-top': '-4px' }}
class="form-message form-message--error"
>
{t(`${nameError()}`)}
</div>
</Show>
</div>
<h4>{t('Address on Discourse')}</h4>
<div class="pretty-form__item">
<div class={styles.discoursName}>
<label for="user-address">https://{hostname()}/author/</label>
<div class={styles.discoursNameField}>
<input
type="text"
name="user-address"
id="user-address"
onInput={(event) => updateFormField('slug', event.currentTarget.value)}
value={form.slug}
ref={(el) => (slugInputRef.current = el)}
class="nolabel"
/>
<Show when={slugError()}>
<p class="form-message form-message--error">{t(`${slugError()}`)}</p>
</Show>
</div>
</div>
</div>
<h4>{t('Introduce')}</h4>
<GrowingTextarea
variant="bordered"
placeholder={t('Introduce')}
value={(value) => updateFormField('bio', value)}
initialValue={form.bio || ''}
allowEnterKey={false}
maxLength={120}
/>
<h4>{t('About')}</h4>
<SimplifiedEditor
variant="bordered"
onlyBubbleControls={true}
smallHeight={true}
placeholder={t('About')}
label={t('About')}
initialContent={form.about || ''}
autoFocus={false}
onChange={(value) => updateFormField('about', value)}
/>
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
<div class={styles.multipleControlsHeader}>
<h4>{t('Social networks')}</h4>
<button type="button" class="button" onClick={() => setAddLinkForm(!addLinkForm())}>
+
</button>
</div>
<Show when={addLinkForm()}>
<SocialNetworkInput
isExist={false}
autofocus={true}
handleInput={(value) => handleChangeSocial(value)}
/>
<Show when={incorrectUrl()}>
<p class="form-message form-message--error">{t('It does not look like url')}</p>
</Show>
</Show>
<For each={profileSocialLinks(social())}>
{(network) => (
<SocialNetworkInput
class={styles.socialInput}
link={network.link}
network={network.name}
handleInput={(value) => handleChangeSocial(value)}
isExist={!network.isPlaceholder}
slug={form.slug}
handleDelete={() => handleDeleteSocialLink(network.link)}
/>
)}
</For>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<Show when={isFloatingPanelVisible()}>
<div class={styles.formActions}>
<div class="wide-container">
<div class="row">
<div class="col-md-19 offset-md-5">
<div class="row">
<div class="col-md-20 col-lg-18 col-xl-16">
<div class={styles.content}>
<Button variant="light" value={t('Cancel')} onClick={handleCancel} />
<Button onClick={handleSubmit} variant="primary" value={t('Save settings')} />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</Show>
</>
</Show>
)
}

View File

@ -0,0 +1 @@
export { ProfileSettings } from './ProfileSettings'

View File

@ -28,8 +28,9 @@ const GrowingTextarea = (props: Props) => {
setValue(props.initialValue ?? '')
}
})
const handleChangeValue = (event) => {
setValue(event.target.value)
const handleChangeValue = (textareaValue) => {
setValue(textareaValue)
props.value(textareaValue)
}
const handleKeyDown = async (event) => {
@ -66,8 +67,7 @@ const GrowingTextarea = (props: Props) => {
: props.initialValue
}
onKeyDown={props.allowEnterKey ? handleKeyDown : null}
onInput={(event) => handleChangeValue(event)}
onChange={(event) => props.value(event.target.value)}
onInput={(event) => handleChangeValue(event.target.value)}
placeholder={props.placeholder}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}

View File

@ -10,7 +10,7 @@ type Props = {
network?: string
link?: string
isExist: boolean
handleChange: (value: string) => void
handleInput: (value: string) => void
handleDelete?: () => void
slug?: string
autofocus?: boolean
@ -33,7 +33,7 @@ export const SocialNetworkInput = (props: Props) => {
class={styles.input}
type="text"
value={props.isExist ? props.link : null}
onChange={(event) => props.handleChange(event.currentTarget.value)}
onInput={(event) => props.handleInput(event.currentTarget.value)}
placeholder={props.autofocus ? null : `${props.link}${props.slug}`}
/>
<Show when={props.isExist}>

View File

@ -1,6 +1,6 @@
import type { ProfileInput } from '../graphql/types.gen'
import { createEffect, createMemo, createSignal } from 'solid-js'
import { createContext, createEffect, createMemo, JSX, useContext } from 'solid-js'
import { createStore } from 'solid-js/store'
import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
@ -8,54 +8,58 @@ import { apiClient } from '../utils/apiClient'
import { useSession } from './session'
type ProfileFormContextType = {
form: ProfileInput
actions: {
setForm: (profile: ProfileInput) => void
submit: (profile: ProfileInput) => Promise<void>
updateFormField: (fieldName: string, value: string, remove?: boolean) => void
}
}
const ProfileFormContext = createContext<ProfileFormContextType>()
export function useProfileForm() {
return useContext(ProfileFormContext)
}
const userpicUrl = (userpic: string) => {
if (userpic.includes('assets.discours.io')) {
return userpic.replace('100x', '500x500')
}
return userpic
}
const useProfileForm = () => {
export const ProfileFormProvider = (props: { children: JSX.Element }) => {
const { session } = useSession()
const [form, setForm] = createStore<ProfileInput>({})
const currentSlug = createMemo(() => session()?.user?.slug)
const { authorEntities } = useAuthorsStore({ authors: [] })
const currentAuthor = createMemo(() => authorEntities()[currentSlug()])
const [slugError, setSlugError] = createSignal<string>()
const submit = async (profile: ProfileInput) => {
const response = await apiClient.updateProfile(profile)
if (response.error) {
setSlugError(response.error)
return response.error
try {
await apiClient.updateProfile(profile)
} catch (error) {
console.error('[ProfileFormProvider]', error)
throw error
}
return response
}
const [form, setForm] = createStore<ProfileInput>({
name: '',
bio: '',
about: '',
slug: '',
userpic: '',
links: [],
})
createEffect(async () => {
if (!currentSlug()) return
try {
await loadAuthor({ slug: currentSlug() })
const currentAuthor = await loadAuthor({ slug: currentSlug() })
setForm({
name: currentAuthor()?.name,
slug: currentAuthor()?.slug,
bio: currentAuthor()?.bio,
about: currentAuthor()?.about,
userpic: userpicUrl(currentAuthor()?.userpic),
links: currentAuthor()?.links,
name: currentAuthor.name,
slug: currentAuthor.slug,
bio: currentAuthor.bio,
about: currentAuthor.about,
userpic: userpicUrl(currentAuthor.userpic),
links: currentAuthor.links,
})
} catch (error) {
console.error(error)
}
})
const updateFormField = (fieldName: string, value: string, remove?: boolean) => {
if (fieldName === 'links') {
if (remove) {
@ -73,7 +77,14 @@ const useProfileForm = () => {
}
}
return { form, submit, updateFormField, slugError }
}
const value: ProfileFormContextType = {
form,
actions: {
submit,
updateFormField,
setForm,
},
}
export { useProfileForm }
return <ProfileFormContext.Provider value={value}>{props.children}</ProfileFormContext.Provider>
}

View File

@ -279,3 +279,25 @@ h5 {
.socialInput {
margin-top: 1rem;
}
.formActions {
background: var(--background-color);
position: sticky;
z-index: 12;
bottom: 0;
border-top: 2px solid var(--black-100);
margin-bottom: -40px;
.content {
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: row;
padding: 1rem 0;
gap: 1rem;
}
.cancel {
margin-right: auto;
}
}

View File

@ -1,331 +1,18 @@
import { createFileUploader } from '@solid-primitives/upload'
import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal'
import { For, createSignal, Show, onMount, onCleanup, createEffect, Switch, Match, lazy } from 'solid-js'
import { createStore } from 'solid-js/store'
import FloatingPanel from '../../components/_shared/FloatingPanel/FloatingPanel'
import { Icon } from '../../components/_shared/Icon'
import { Loading } from '../../components/_shared/Loading'
import { PageLayout } from '../../components/_shared/PageLayout'
import { Popover } from '../../components/_shared/Popover'
import { SocialNetworkInput } from '../../components/_shared/SocialNetworkInput'
import { AuthGuard } from '../../components/AuthGuard'
import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
import { ProfileSettings } from '../../components/ProfileSettings'
import { useLocalize } from '../../context/localize'
import { useProfileForm } from '../../context/profile'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { clone } from '../../utils/clone'
import { getImageUrl } from '../../utils/getImageUrl'
import { handleImageUpload } from '../../utils/handleImageUpload'
import { profileSocialLinks } from '../../utils/profileSocialLinks'
import { validateUrl } from '../../utils/validateUrl'
import styles from './Settings.module.scss'
const SimplifiedEditor = lazy(() => import('../../components/Editor/SimplifiedEditor'))
const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTextarea/GrowingTextarea'))
import { ProfileFormProvider } from '../../context/profile'
export const ProfileSettingsPage = () => {
const { t } = useLocalize()
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const [isUserpicUpdating, setIsUserpicUpdating] = createSignal(false)
const [uploadError, setUploadError] = createSignal(false)
const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { loadSession },
} = useSession()
const { form, updateFormField, submit, slugError } = useProfileForm()
const [prevForm, setPrevForm] = createStore(clone(form))
const [social, setSocial] = createSignal(form.links)
const handleChangeSocial = (value: string) => {
if (validateUrl(value)) {
updateFormField('links', value)
setAddLinkForm(false)
} else {
setIncorrectUrl(true)
}
}
const handleSubmit = async (event: Event) => {
event.preventDefault()
try {
await submit(form)
setPrevForm(clone(form))
showSnackbar({ body: t('Profile successfully saved') })
} catch {
showSnackbar({ type: 'error', body: t('Error') })
}
loadSession()
}
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const handleUploadAvatar = async () => {
selectFiles(async ([uploadFile]) => {
try {
setUploadError(false)
setIsUserpicUpdating(true)
const result = await handleImageUpload(uploadFile)
updateFormField('userpic', result.url)
setIsUserpicUpdating(false)
setIsFloatingPanelVisible(true)
} catch (error) {
setUploadError(true)
console.error('[upload avatar] error', error)
}
})
}
const [hostname, setHostname] = createSignal<string | null>(null)
onMount(() => {
setHostname(window?.location.host)
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleBeforeUnload = (event) => {
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))
})
const handleSaveProfile = () => {
setIsFloatingPanelVisible(false)
setPrevForm(clone(form))
}
createEffect(() => {
if (!deepEqual(form, prevForm)) {
setIsFloatingPanelVisible(true)
}
})
const handleDeleteSocialLink = (link) => {
updateFormField('links', link, true)
}
createEffect(() => {
setSocial(form.links)
})
return (
<PageLayout title={t('Profile')}>
<AuthGuard>
<Show when={form}>
<div class="wide-container">
<div class="row">
<div class="col-md-5">
<div class={clsx('left-navigation', styles.leftNavigation)}>
<ProfileSettingsNavigation />
</div>
</div>
<div class="col-md-19">
<div class="row">
<div class="col-md-20 col-lg-18 col-xl-16">
<h1>{t('Profile settings')}</h1>
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
<form onSubmit={handleSubmit} enctype="multipart/form-data">
<h4>{t('Userpic')}</h4>
<div class="pretty-form__item">
<div
class={clsx(styles.userpic, { [styles.hasControls]: form.userpic })}
onClick={!form.userpic && handleUploadAvatar}
>
<Switch>
<Match when={isUserpicUpdating()}>
<Loading />
</Match>
<Match when={form.userpic}>
<div
class={styles.userpicImage}
style={{
'background-image': `url(${getImageUrl(form.userpic, {
width: 180,
height: 180,
})})`,
}}
/>
<div class={styles.controls}>
<Popover content={t('Delete userpic')}>
{(triggerRef: (el) => void) => (
<button
ref={triggerRef}
class={styles.control}
onClick={() => updateFormField('userpic', '')}
>
<Icon name="close" />
</button>
)}
</Popover>
<Popover content={t('Upload userpic')}>
{(triggerRef: (el) => void) => (
<button
ref={triggerRef}
class={styles.control}
onClick={handleUploadAvatar}
>
<Icon name="user-image-black" />
</button>
)}
</Popover>
</div>
</Match>
<Match when={!form.userpic}>
<Icon name="user-image-gray" />
{t('Here you can upload your photo')}
</Match>
</Switch>
</div>
<Show when={uploadError()}>
<div class={styles.error}>{t('Upload error')}</div>
</Show>
</div>
<h4>{t('Name')}</h4>
<p class="description">
{t(
'Your name will appear on your profile page and as your signature in publications, comments and responses.',
)}
</p>
<div class="pretty-form__item">
<input
type="text"
name="username"
id="username"
placeholder={t('Name')}
onChange={(event) => updateFormField('name', event.currentTarget.value)}
value={form.name}
/>
<label for="username">{t('Name')}</label>
</div>
<h4>{t('Address on Discourse')}</h4>
<div class="pretty-form__item">
<div class={styles.discoursName}>
<label for="user-address">https://{hostname()}/author/</label>
<div class={styles.discoursNameField}>
<input
type="text"
name="user-address"
id="user-address"
onChange={(event) => updateFormField('slug', event.currentTarget.value)}
value={form.slug}
class="nolabel"
/>
<Show when={slugError()}>
<p class="form-message form-message--error">{t(`${slugError()}`)}</p>
</Show>
</div>
</div>
</div>
<h4>{t('Introduce')}</h4>
<GrowingTextarea
variant="bordered"
placeholder={t('Introduce')}
value={(value) => updateFormField('bio', value)}
initialValue={form.bio}
allowEnterKey={false}
maxLength={120}
/>
<h4>{t('About')}</h4>
<SimplifiedEditor
variant="bordered"
onlyBubbleControls={true}
smallHeight={true}
placeholder={t('About')}
label={t('About')}
initialContent={form.about}
onChange={(value) => updateFormField('about', value)}
/>
{/*Нет реализации полей на бэке*/}
{/*<h4>{t('How can I help/skills')}</h4>*/}
{/*<div class="pretty-form__item">*/}
{/* <input type="text" name="skills" id="skills" />*/}
{/*</div>*/}
{/*<h4>{t('Where')}</h4>*/}
{/*<div class="pretty-form__item">*/}
{/* <input type="text" name="location" id="location" placeholder="Откуда" />*/}
{/* <label for="location">{t('Where')}</label>*/}
{/*</div>*/}
{/*<h4>{t('Date of Birth')}</h4>*/}
{/*<div class="pretty-form__item">*/}
{/* <input*/}
{/* type="date"*/}
{/* name="birthdate"*/}
{/* id="birthdate"*/}
{/* placeholder="Дата рождения"*/}
{/* class="nolabel"*/}
{/* />*/}
{/*</div>*/}
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
<div class={styles.multipleControlsHeader}>
<h4>{t('Social networks')}</h4>
<button
type="button"
class="button"
onClick={() => setAddLinkForm(!addLinkForm())}
>
+
</button>
</div>
<Show when={addLinkForm()}>
<SocialNetworkInput
isExist={false}
autofocus={true}
handleChange={(value) => handleChangeSocial(value)}
/>
<Show when={incorrectUrl()}>
<p class="form-message form-message--error">{t('It does not look like url')}</p>
</Show>
</Show>
<For each={profileSocialLinks(social())}>
{(network) => (
<SocialNetworkInput
class={styles.socialInput}
link={network.link}
network={network.name}
handleChange={(value) => handleChangeSocial(value)}
isExist={!network.isPlaceholder}
slug={form.slug}
handleDelete={() => handleDeleteSocialLink(network.link)}
/>
)}
</For>
</div>
<br />
<FloatingPanel
isVisible={isFloatingPanelVisible()}
confirmTitle={t('Save settings')}
confirmAction={handleSaveProfile}
declineTitle={t('Cancel')}
declineAction={() => setIsFloatingPanelVisible(false)}
/>
</form>
</div>
</div>
</div>
</div>
</div>
</Show>
<ProfileFormProvider>
<ProfileSettings />
</ProfileFormProvider>
</AuthGuard>
</PageLayout>
)

View File

@ -57,9 +57,10 @@ const addAuthors = (authors: Author[]) => {
)
}
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
export const loadAuthor = async ({ slug }: { slug: string }): Promise<Author> => {
const author = await apiClient.getAuthor({ slug })
addAuthors([author])
return author
}
export const addAuthorsByTopic = (newAuthorsByTopic: { [topicSlug: string]: Author[] }) => {

View File

@ -70,6 +70,7 @@ type ApiErrorCode =
| 'user_already_exists'
| 'token_expired'
| 'token_invalid'
| 'duplicate_slug'
export class ApiError extends Error {
code: ApiErrorCode
@ -249,6 +250,15 @@ export const apiClient = {
},
updateProfile: async (input: ProfileInput) => {
const response = await privateGraphQLClient.mutation(updateProfile, { profile: input }).toPromise()
if (response.error) {
if (
response.error.message.includes('duplicate key value violates unique constraint "user_slug_key"')
) {
throw new ApiError('duplicate_slug', response.error.message)
}
throw new ApiError('unknown', response.error.message)
}
return response.data.updateProfile
},
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {