From cb5c78790bb363eee2a4e9eb011d95c1603fdabe Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Mon, 13 May 2024 02:36:46 +0300 Subject: [PATCH 01/20] Feature/profile settings page (#452) * Init change password form --- public/locales/en/translation.json | 10 +- public/locales/ru/translation.json | 10 +- src/components/Editor/SimplifiedEditor.tsx | 8 +- .../AuthModal/PasswordField/PasswordField.tsx | 19 +- .../ProfileSettings/ProfileSettings.tsx | 17 +- src/context/session.tsx | 5 + src/pages/profile/Settings.module.scss | 20 +- src/pages/profile/profileSecurity.page.tsx | 414 +++++++++++++----- 8 files changed, 364 insertions(+), 139 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 69ce9f96..f8b6760e 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -546,5 +546,13 @@ "It's OK. Just enter your email to receive a link to change your password": "It's OK. Just enter your email to receive a link to change your password", "Restore password": "Restore password", "Subscribing...": "Subscribing...", - "Unsubscribing...": "Unsubscribing..." + "Unsubscribing...": "Unsubscribing...", + "Login and security": "Login and security", + "Settings for account, email, password and login methods.": "Settings for account, email, password and login methods.", + "Current password": "Current password", + "Confirm your new password": "Confirm your new password", + "Connect": "Connect", + "Incorrect old password": "Incorrect old password", + "Repeat new password": "Repeat new password", + "Incorrect new password confirm": "Incorrect new password confirm" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 445c7906..65a38874 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -573,5 +573,13 @@ "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", "Restore password": "Восстановить пароль", "Subscribing...": "Подписываем...", - "Unsubscribing...": "Отписываем..." + "Unsubscribing...": "Отписываем...", + "Login and security": "Вход и безопасность", + "Settings for account, email, password and login methods.": "Настройки аккаунта, почты, пароля и способов входа.", + "Current password": "Текущий пароль", + "Confirm your new password": "Подтвердите новый пароль", + "Connect": "Привязать", + "Incorrect old password": "Старый пароль не верен", + "Repeat new password": "Повторите новый пароль", + "Incorrect new password confirm": "Неверное подтверждение нового пароля" } diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 187fe25f..18e8792c 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -48,11 +48,13 @@ type Props = { onChange?: (text: string) => void variant?: 'minimal' | 'bordered' maxLength?: number + noLimits?: boolean maxHeight?: number submitButtonText?: string quoteEnabled?: boolean imageEnabled?: boolean setClear?: boolean + resetToInitial?: boolean smallHeight?: boolean submitByCtrlEnter?: boolean onlyBubbleControls?: boolean @@ -124,7 +126,7 @@ const SimplifiedEditor = (props: Props) => { openOnClick: false, }), CharacterCount.configure({ - limit: maxLength, + limit: props.noLimits ? null : maxLength, }), Blockquote.configure({ HTMLAttributes: { @@ -216,6 +218,10 @@ const SimplifiedEditor = (props: Props) => { if (props.setClear) { editor().commands.clearContent(true) } + if (props.resetToInitial) { + editor().commands.clearContent(true) + editor().commands.setContent(props.initialContent) + } }) const handleKeyDown = (event) => { diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx index a478225d..a2ec9185 100644 --- a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -16,6 +16,9 @@ type Props = { onBlur?: (value: string) => void variant?: 'login' | 'registration' disableAutocomplete?: boolean + noValidate?: boolean + onFocus?: () => void + value?: string } const minLength = 8 @@ -27,7 +30,7 @@ export const PasswordField = (props: Props) => { const [showPassword, setShowPassword] = createSignal(false) const [error, setError] = createSignal() - const validatePassword = (passwordToCheck) => { + const validatePassword = (passwordToCheck: string) => { if (passwordToCheck.length < minLength) { return t('Password should be at least 8 characters') } @@ -50,11 +53,13 @@ export const PasswordField = (props: Props) => { } props.onInput(value) - const errorValue = validatePassword(value) - if (errorValue) { - setError(errorValue) - } else { - setError() + if (!props.noValidate) { + const errorValue = validatePassword(value) + if (errorValue) { + setError(errorValue) + } else { + setError() + } } } @@ -78,6 +83,8 @@ export const PasswordField = (props: Props) => { id="password" name="password" disabled={props.disabled} + onFocus={props.onFocus} + value={props.value ? props.value : ''} autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'} type={showPassword() ? 'text' : 'password'} placeholder={props.placeholder || t('Password')} diff --git a/src/components/ProfileSettings/ProfileSettings.tsx b/src/components/ProfileSettings/ProfileSettings.tsx index 15532ddb..a74597a5 100644 --- a/src/components/ProfileSettings/ProfileSettings.tsx +++ b/src/components/ProfileSettings/ProfileSettings.tsx @@ -20,6 +20,8 @@ import { useLocalize } from '../../context/localize' import { useProfileForm } from '../../context/profile' import { useSession } from '../../context/session' import { useSnackbar } from '../../context/snackbar' +import { ProfileInput } from '../../graphql/schema/core.gen' +import styles from '../../pages/profile/Settings.module.scss' import { hideModal, showModal } from '../../stores/ui' import { clone } from '../../utils/clone' import { getImageUrl } from '../../utils/getImageUrl' @@ -35,14 +37,12 @@ import { Loading } from '../_shared/Loading' import { Popover } from '../_shared/Popover' import { SocialNetworkInput } from '../_shared/SocialNetworkInput' -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 [prevForm, setPrevForm] = createStore({}) const [isFormInitialized, setIsFormInitialized] = createSignal(false) const [isSaving, setIsSaving] = createSignal(false) const [social, setSocial] = createSignal([]) @@ -59,6 +59,7 @@ export const ProfileSettings = () => { const { showSnackbar } = useSnackbar() const { loadAuthor, session } = useSession() const { showConfirm } = useConfirm() + const [clearAbout, setClearAbout] = createSignal(false) createEffect(() => { if (Object.keys(form).length > 0 && !isFormInitialized()) { @@ -121,7 +122,9 @@ export const ProfileSettings = () => { declineButtonVariant: 'secondary', }) if (isConfirmed) { + setClearAbout(true) setForm(clone(prevForm)) + setClearAbout(false) } } @@ -171,11 +174,13 @@ export const ProfileSettings = () => { on( () => deepEqual(form, prevForm), () => { - setIsFloatingPanelVisible(!deepEqual(form, prevForm)) + if (Object.keys(prevForm).length > 0) { + setIsFloatingPanelVisible(!deepEqual(form, prevForm)) + } }, - { defer: true }, ), ) + const handleDeleteSocialLink = (link) => { updateFormField('links', link, true) } @@ -317,6 +322,8 @@ export const ProfileSettings = () => {

{t('About')}

void signUp: (params: SignupInput) => Promise<{ data: AuthToken; errors: Error[] }> signIn: (params: LoginInput) => Promise<{ data: AuthToken; errors: Error[] }> + updateProfile: (params: UpdateProfileInput) => Promise<{ data: AuthToken; errors: Error[] }> signOut: () => Promise oauth: (provider: string) => Promise forgotPassword: ( @@ -305,6 +307,8 @@ export const SessionProvider = (props: { } const signUp = async (params: SignupInput) => await authenticate(authorizer().signup, params) const signIn = async (params: LoginInput) => await authenticate(authorizer().login, params) + const updateProfile = async (params: UpdateProfileInput) => + await authenticate(authorizer().updateProfile, params) const signOut = async () => { const authResult: ApiResponse = await authorizer().logout() @@ -381,6 +385,7 @@ export const SessionProvider = (props: { signIn, signOut, confirmEmail, + updateProfile, setIsSessionLoaded, setSession, setAuthor, diff --git a/src/pages/profile/Settings.module.scss b/src/pages/profile/Settings.module.scss index 9bd4906c..38babb4d 100644 --- a/src/pages/profile/Settings.module.scss +++ b/src/pages/profile/Settings.module.scss @@ -100,17 +100,6 @@ h5 { } } -.passwordToggleControl { - position: absolute; - right: 1em; - transform: translateY(-50%); - top: 50%; -} - -.passwordInput { - padding-right: 3em !important; -} - .searchContainer { margin-top: 2.4rem; } @@ -331,3 +320,12 @@ div[data-lastpass-infield="true"] { opacity: 0 !important; } + +.emailValidationError { + position: absolute; + top: 100%; + font-size: 12px; + line-height: 16px; + margin-top: 0.3em; + color: var(--danger-color); +} diff --git a/src/pages/profile/profileSecurity.page.tsx b/src/pages/profile/profileSecurity.page.tsx index 2e572047..b19f139e 100644 --- a/src/pages/profile/profileSecurity.page.tsx +++ b/src/pages/profile/profileSecurity.page.tsx @@ -6,135 +6,321 @@ import { Icon } from '../../components/_shared/Icon' import { PageLayout } from '../../components/_shared/PageLayout' import { useLocalize } from '../../context/localize' +import { UpdateProfileInput } from '@authorizerdev/authorizer-js' +import { Show, createEffect, createSignal, on } from 'solid-js' +import { PasswordField } from '../../components/Nav/AuthModal/PasswordField' +import { Button } from '../../components/_shared/Button' +import { Loading } from '../../components/_shared/Loading' +import { useConfirm } from '../../context/confirm' +import { useSession } from '../../context/session' +import { useSnackbar } from '../../context/snackbar' +import { DEFAULT_HEADER_OFFSET } from '../../stores/router' +import { validateEmail } from '../../utils/validateEmail' import styles from './Settings.module.scss' +type FormField = 'oldPassword' | 'newPassword' | 'newPasswordConfirm' | 'email' export const ProfileSecurityPage = () => { const { t } = useLocalize() + const { updateProfile, session, isSessionLoaded } = useSession() + const { showSnackbar } = useSnackbar() + const { showConfirm } = useConfirm() + + const [newPasswordError, setNewPasswordError] = createSignal() + const [oldPasswordError, setOldPasswordError] = createSignal() + const [emailError, setEmailError] = createSignal() + const [isSubmitting, setIsSubmitting] = createSignal() + const [isFloatingPanelVisible, setIsFloatingPanelVisible] = createSignal(false) + + const initialState = { + oldPassword: undefined, + newPassword: undefined, + newPasswordConfirm: undefined, + email: undefined, + } + const [formData, setFormData] = createSignal(initialState) + const oldPasswordRef: { current: HTMLDivElement } = { current: null } + const newPasswordRepeatRef: { current: HTMLDivElement } = { current: null } + + createEffect( + on( + () => session()?.user?.email, + () => { + setFormData((prevData) => ({ + ...prevData, + ['email']: session()?.user?.email, + })) + }, + ), + ) + const handleInputChange = (name: FormField, value: string) => { + if ( + name === 'email' || + (name === 'newPasswordConfirm' && value && value?.length > 0 && !emailError() && !newPasswordError()) + ) { + setIsFloatingPanelVisible(true) + } else { + setIsFloatingPanelVisible(false) + } + setFormData((prevData) => ({ + ...prevData, + [name]: value, + })) + } + + const handleCancel = async () => { + const isConfirmed = await showConfirm({ + confirmBody: t('Do you really want to reset all changes?'), + confirmButtonVariant: 'primary', + declineButtonVariant: 'secondary', + }) + if (isConfirmed) { + setEmailError() + setFormData({ + ...initialState, + ['email']: session()?.user?.email, + }) + setIsFloatingPanelVisible(false) + } + } + const handleChangeEmail = (_value: string) => { + if (!validateEmail(formData()['email'])) { + setEmailError(t('Invalid email')) + return + } + } + const handleCheckNewPassword = (value: string) => { + handleInputChange('newPasswordConfirm', value) + if (value !== formData()['newPassword']) { + const rect = newPasswordRepeatRef.current.getBoundingClientRect() + const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2 + window.scrollTo({ + top: topPosition, + left: 0, + behavior: 'smooth', + }) + showSnackbar({ type: 'error', body: t('Incorrect new password confirm') }) + setNewPasswordError(t('Passwords are not equal')) + } + } + + const handleSubmit = async () => { + setIsSubmitting(true) + + const options: UpdateProfileInput = { + old_password: formData()['oldPassword'], + new_password: formData()['newPassword'] || formData()['oldPassword'], + confirm_new_password: formData()['newPassword'] || formData()['oldPassword'], + email: formData()['email'], + } + + try { + const { errors } = await updateProfile(options) + if (errors.length > 0) { + console.error(errors) + if (errors.some((obj) => obj.message === 'incorrect old password')) { + setOldPasswordError(t('Incorrect old password')) + showSnackbar({ type: 'error', body: t('Incorrect old password') }) + const rect = oldPasswordRef.current.getBoundingClientRect() + const topPosition = window.scrollY + rect.top - DEFAULT_HEADER_OFFSET * 2 + window.scrollTo({ + top: topPosition, + left: 0, + behavior: 'smooth', + }) + setIsFloatingPanelVisible(false) + } + return + } + showSnackbar({ type: 'success', body: t('Profile successfully saved') }) + } catch (error) { + console.error(error) + } finally { + setIsSubmitting(false) + } + } return ( -
-
-
-
- + }> +
+
+
+
+ +
-
-
-
-
-

Вход и безопасность

-

Настройки аккаунта, почты, пароля и способов входа.

- -
-

Почта

-
- - -
- -

Изменить пароль

-
Текущий пароль
-
- - -
- -
Новый пароль
-
- - -
- -
Подтвердите новый пароль
-
- - -
- -

Социальные сети

-
Google
-
-

- -

-
- -
VK
-
-

- -

-
- -
Facebook
-
-

- -

-
- -
Apple
-
-

- -

-
- -
-

- +

+
+
+

{t('Login and security')}

+

+ {t('Settings for account, email, password and login methods.')}

- + +
+

{t('Email')}

+
+ setEmailError()} + onInput={(event) => handleChangeEmail(event.target.value)} + /> + + +
+ {emailError()} +
+
+
+ +

{t('Change password')}

+
{t('Current password')}
+ +
(oldPasswordRef.current = el)}> + setOldPasswordError()} + setError={oldPasswordError()} + onInput={(value) => handleInputChange('oldPassword', value)} + value={formData()['oldPassword'] ?? null} + disabled={isSubmitting()} + /> +
+ +
{t('New password')}
+ { + handleInputChange('newPassword', value) + handleInputChange('newPasswordConfirm', '') + }} + value={formData()['newPassword'] ?? ''} + disabled={isSubmitting()} + disableAutocomplete={true} + /> + +
{t('Confirm your new password')}
+
(newPasswordRepeatRef.current = el)}> + 0 + ? formData()['newPasswordConfirm'] + : null + } + onFocus={() => setNewPasswordError()} + setError={newPasswordError()} + onInput={(value) => handleCheckNewPassword(value)} + disabled={isSubmitting()} + disableAutocomplete={true} + /> +
+

{t('Social networks')}

+
Google
+
+

+ +

+
+ +
VK
+
+

+ +

+
+ +
Facebook
+
+

+ +

+
+ +
Apple
+
+

+ +

+
+ +
-
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) From ed2b4ebfbf08eaa2183645ab549bddd7c6bbc301 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 13:44:43 +0300 Subject: [PATCH 02/20] stab-hotfix --- src/components/Article/FullArticle.tsx | 2 +- src/components/Author/AuthorBadge/AuthorBadge.tsx | 2 +- src/components/Author/AuthorCard/AuthorCard.tsx | 2 +- src/components/Draft/Draft.tsx | 2 +- src/components/Feed/ArticleCard/ArticleCard.tsx | 2 +- src/pages/create.page.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index eb10ea09..3cea62c2 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -538,7 +538,7 @@ export const FullArticle = (props: Props) => { {(triggerRef: (el) => void) => (
diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 665f300f..bcb29a29 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => { requireAuthentication(() => { openPage(router, 'inbox') changeSearchParams({ - initChat: props.author.id.toString(), + initChat: props.author?.id.toString(), }) }, 'discussions') } diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index c3b1ccd3..f1b58fef 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -65,7 +65,7 @@ export const AuthorCard = (props: Props) => { requireAuthentication(() => { openPage(router, 'inbox') changeSearchParams({ - initChat: props.author.id.toString(), + initChat: props.author?.id.toString(), }) }, 'discussions') } diff --git a/src/components/Draft/Draft.tsx b/src/components/Draft/Draft.tsx index cfbc9b0c..b66b9c1a 100644 --- a/src/components/Draft/Draft.tsx +++ b/src/components/Draft/Draft.tsx @@ -60,7 +60,7 @@ export const Draft = (props: Props) => {
{t('Edit')} diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 19529050..9fad27b5 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -328,7 +328,7 @@ export const ArticleCard = (props: ArticleCardProps) => { {(triggerRef: (el) => void) => (
- + { const shout = await apiClient.createArticle({ article: { layout: layout } }) redirectPage(router, 'edit', { - shoutId: shout.id.toString(), + shoutId: shout?.id.toString(), }) } From aa8d5973ee67cea5352a7da157bf3e32378700f8 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 13:45:35 +0300 Subject: [PATCH 03/20] swiper-fix --- src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx index 067748d5..c614cba1 100644 --- a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx @@ -37,7 +37,6 @@ export const ArticleCardSwiper = (props: Props) => { [styles.Swiper]: props.slides.length > 1, [styles.articleMode]: true, [styles.ArticleCardSwiper]: props.slides.length > 1, - [styles.unswiped]: props.slides.length === 1, })} > From 150903ecbdaff280912edcb324c2b5951b20df4f Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 14:25:37 +0300 Subject: [PATCH 04/20] update-token-fix --- src/context/session.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/context/session.tsx b/src/context/session.tsx index 45b79f79..27ed21e5 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -225,9 +225,12 @@ export const SessionProvider = (props: { const appdata = session()?.user.app_data if (appdata) { const { profile } = appdata - setAuthor(profile) - addAuthors([profile]) - if (!profile) loadAuthor() + if (profile?.id) { + setAuthor(profile) + addAuthors([profile]) + } else { + setTimeout(loadAuthor, 15) + } } } catch (e) { console.error(e) From 3f96850948df48e2e95ff4c5e60b507d92a9fefb Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 15:46:51 +0300 Subject: [PATCH 05/20] connect-logs-fix --- src/context/connect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context/connect.tsx b/src/context/connect.tsx index dfd8d549..3d2459bd 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -61,10 +61,10 @@ export const ConnectProvider = (props: { children: JSX.Element }) => { if (response.ok && response.headers.get('content-type') === EventStreamContentType) { setConnected(true) } else if (response.status === 401) { - throw new Error('unauthorized') + throw new Error('cannot connect to real-time updates') } else { setRetried((r) => r + 1) - throw new Error('Internal Error') + throw new Error(`failed to connect ${retried()} times`) } }, onclose() { From c61ad862342e98856da970143ca193d57f0c049a Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 15:47:37 +0300 Subject: [PATCH 06/20] sse --- src/context/connect.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/context/connect.tsx b/src/context/connect.tsx index 3d2459bd..2b2dc808 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -61,10 +61,10 @@ export const ConnectProvider = (props: { children: JSX.Element }) => { if (response.ok && response.headers.get('content-type') === EventStreamContentType) { setConnected(true) } else if (response.status === 401) { - throw new Error('cannot connect to real-time updates') + throw new Error('SSE: cannot connect to real-time updates') } else { setRetried((r) => r + 1) - throw new Error(`failed to connect ${retried()} times`) + throw new Error(`SSE: failed to connect ${retried()} times`) } }, onclose() { From c8517c85c6f3ee09fa3b277fc245e3cfa4d57209 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 20:36:22 +0300 Subject: [PATCH 07/20] edit-hotfix --- src/pages/edit.page.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pages/edit.page.tsx b/src/pages/edit.page.tsx index 21353681..f729e50b 100644 --- a/src/pages/edit.page.tsx +++ b/src/pages/edit.page.tsx @@ -1,4 +1,4 @@ -import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js' +import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on } from 'solid-js' import { AuthGuard } from '../components/AuthGuard' import { Loading } from '../components/_shared/Loading' @@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize' import { useSession } from '../context/session' import { apiClient } from '../graphql/client/core' import { Shout } from '../graphql/schema/core.gen' -import { router } from '../stores/router' +import { router, useRouter } from '../stores/router' import { redirectPage } from '@nanostores/router' import { useSnackbar } from '../context/snackbar' @@ -33,6 +33,7 @@ const getContentTypeTitle = (layout: LayoutType) => { export const EditPage = () => { const { t } = useLocalize() const { session } = useSession() + const { page } = useRouter() const snackbar = useSnackbar() const fail = async (error: string) => { @@ -45,12 +46,18 @@ export const EditPage = () => { const [shoutId, setShoutId] = createSignal(0) const [shout, setShout] = createSignal() - onMount(() => { - const shoutId = window.location.pathname.split('/').pop() - const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) - console.debug(`editing shout ${shoutIdFromUrl}`) - if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) - }) + createEffect( + on( + page, + (p) => { + const shoutId = p?.path.split('/').pop() + const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) + console.debug(`editing shout ${shoutIdFromUrl}`) + if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) + }, + { defer: true }, + ), + ) createEffect( on([session, shout, shoutId], async ([ses, sh, shid]) => { From 95c4d777b221ef6df5dfc916d2adfad09490f572 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 20:43:20 +0300 Subject: [PATCH 08/20] connect-fox --- src/context/connect.tsx | 78 ++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/src/context/connect.tsx b/src/context/connect.tsx index 2b2dc808..f8ebb1c9 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -30,53 +30,57 @@ const ConnectContext = createContext() export const ConnectProvider = (props: { children: JSX.Element }) => { const [messageHandlers, setHandlers] = createSignal([]) - // const [messages, setMessages] = createSignal>([]); const [connected, setConnected] = createSignal(false) const { session } = useSession() + const [retried, setRetried] = createSignal(0) const addHandler = (handler: MessageHandler) => { setHandlers((hhh) => [...hhh, handler]) } - const [retried, setRetried] = createSignal(0) createEffect(async () => { const token = session()?.access_token - if (token && !connected()) { + if (token && !connected() && retried() <= RECONNECT_TIMES) { console.info('[context.connect] init SSE connection') - await fetchEventSource('https://connect.discours.io', { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: token, - }, - onmessage(event) { - const m: SSEMessage = JSON.parse(event.data || '{}') - console.log('[context.connect] Received message:', m) - - // Iterate over all registered handlers and call them - messageHandlers().forEach((handler) => handler(m)) - }, - async onopen(response) { - console.log('[context.connect] SSE connection opened', response) - if (response.ok && response.headers.get('content-type') === EventStreamContentType) { - setConnected(true) - } else if (response.status === 401) { - throw new Error('SSE: cannot connect to real-time updates') - } else { - setRetried((r) => r + 1) - throw new Error(`SSE: failed to connect ${retried()} times`) - } - }, - onclose() { - console.log('[context.connect] SSE connection closed by server') - setConnected(false) - }, - onerror(err) { - if (err.message === 'unauthorized' || retried() > RECONNECT_TIMES) { - throw err // rethrow to stop the operation - } - }, - }) + try { + await fetchEventSource('https://connect.discours.io', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + onmessage(event) { + const m: SSEMessage = JSON.parse(event.data || '{}') + console.log('[context.connect] Received message:', m) + messageHandlers().forEach((handler) => handler(m)) + }, + onopen: (response) => { + console.log('[context.connect] SSE connection opened', response) + if (response.ok && response.headers.get('content-type') === EventStreamContentType) { + setConnected(true) + setRetried(0) + return Promise.resolve() + } + return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`) + }, + onclose() { + console.log('[context.connect] SSE connection closed by server') + setConnected(false) + if (retried() < RECONNECT_TIMES) { + setRetried((r) => r + 1) + } + }, + onerror(err) { + console.error('[context.connect] SSE connection error:', err) + setConnected(false) + if (retried() < RECONNECT_TIMES) { + setRetried((r) => r + 1) + } else throw Error(err) + }, + }) + } catch (error) { + console.error('[context.connect] SSE connection failed:', error) + } } }) From 60664581bdb3f7154ef165305850657454a2b199 Mon Sep 17 00:00:00 2001 From: Untone Date: Sat, 18 May 2024 20:50:27 +0300 Subject: [PATCH 09/20] logfix --- src/components/Nav/AuthModal/LoginForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index c7a26d68..1a50aeda 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -95,8 +95,8 @@ export const LoginForm = () => { try { const { errors } = await signIn({ email: email(), password: password() }) - console.error('[signIn errors]', errors) if (errors?.length > 0) { + console.debug('[signIn] errors:', errors) if ( errors.some( (error) => From 4a55271a79d4935e6e0d94c08900cb53927cdb2e Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 00:48:58 +0300 Subject: [PATCH 10/20] edit-hotfix --- src/pages/edit.page.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pages/edit.page.tsx b/src/pages/edit.page.tsx index f729e50b..aaa08eee 100644 --- a/src/pages/edit.page.tsx +++ b/src/pages/edit.page.tsx @@ -48,14 +48,18 @@ export const EditPage = () => { createEffect( on( - page, + () => page(), (p) => { - const shoutId = p?.path.split('/').pop() - const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) - console.debug(`editing shout ${shoutIdFromUrl}`) - if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) + if (p?.path) { + console.debug(p?.path) + const shoutId = p?.path.split('/').pop() + const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) + console.debug(`editing shout ${shoutIdFromUrl}`) + if (shoutIdFromUrl) { + setShoutId(shoutIdFromUrl) + } + } }, - { defer: true }, ), ) @@ -70,6 +74,7 @@ export const EditPage = () => { } } }), + { defer: true }, ) const title = createMemo(() => { From 135e0d215f922e262061864a41498b1fbae6f940 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 00:55:30 +0300 Subject: [PATCH 11/20] error-catch --- src/components/Nav/AuthModal/LoginForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 1a50aeda..25867cec 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -99,8 +99,7 @@ export const LoginForm = () => { console.debug('[signIn] errors:', errors) if ( errors.some( - (error) => - error.message.includes('bad user credentials') || error.message.includes('user not found'), + (error) => error.message.includes('bad user credentials') || error.message.includes('user not'), ) ) { setValidationErrors((prev) => ({ From f22d10a535529f4d37207ef0be5b6e7b086e8bd9 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Sun, 19 May 2024 01:03:06 +0300 Subject: [PATCH 12/20] Placeholders fixes --- .../Feed/Placeholder/Placeholder.module.scss | 81 +++++++++++++------ .../Feed/Placeholder/Placeholder.tsx | 2 +- src/components/Views/Author/Author.tsx | 42 +++++----- src/components/Views/Feed/Feed.tsx | 9 ++- src/pages/feed.page.tsx | 5 +- 5 files changed, 85 insertions(+), 54 deletions(-) diff --git a/src/components/Feed/Placeholder/Placeholder.module.scss b/src/components/Feed/Placeholder/Placeholder.module.scss index abd61262..42b4e765 100644 --- a/src/components/Feed/Placeholder/Placeholder.module.scss +++ b/src/components/Feed/Placeholder/Placeholder.module.scss @@ -17,6 +17,7 @@ display: flex; @include font-size(1.5rem); gap: 0.6rem; + justify-content: center; margin-top: 3rem; padding: 1rem 2rem; width: 100%; @@ -29,46 +30,63 @@ } .placeholder--feed-mode { - aspect-ratio: 1 / 0.8; flex-direction: column; + min-height: 40rem; text-align: center; - &:after { - bottom: 0; - content: ''; - height: 20%; - left: 0; - position: absolute; - width: 100%; - - .placeholder--feed & { - background: linear-gradient(to top, #171032, rgba(23, 16, 50, 0)); - } - - .placeholder--feedCollaborations & { - background: linear-gradient(to top, #070709, rgba(7, 7, 9, 0)); - } + @include media-breakpoint-up(lg) { + aspect-ratio: 1 / 0.8; } .placeholderCover { - flex: 0 100%; - width: 100%; + flex: 1 100%; + position: relative; + + &:after { + bottom: 0; + content: ''; + height: 20%; + left: 0; + position: absolute; + width: 100%; + } img { position: absolute; } } + + &.placeholder--feedMy .placeholderCover:after { + background: linear-gradient(to top, #171032, rgba(23, 16, 50, 0)); + } + + &.placeholder--feedCollaborations .placeholderCover:after { + background: linear-gradient(to top, #070709, rgba(7, 7, 9, 0)); + } } .placeholder--profile-mode { - min-height: 28rem; + min-height: 40rem; + + @include media-breakpoint-down(md) { + display: block; + } + + @include media-breakpoint-up(md) { + min-height: 28rem; + } .placeholderCover { - flex: 0 45rem; - min-width: 45rem; - order: 2; padding: 1.6rem; + @include media-breakpoint-up(md) { + flex: 0 45rem; + min-width: 50%; + order: 2; + position: static; + width: 45rem; + } + img { height: auto; width: 100%; @@ -92,9 +110,15 @@ background: var(--background-color-invert); color: var(--default-color-invert); bottom: 2rem; - position: absolute; + left: 2rem; right: 2rem; - width: auto; + width: 100%; + + @include media-breakpoint-up(md) { + left: auto; + position: absolute; + width: auto; + } .icon { filter: invert(1); @@ -117,7 +141,7 @@ padding: 1.6rem; } -.placeholder--feed, +.placeholder--feedMy, .placeholder--feedCollaborations { color: var(--default-color-invert); @@ -128,7 +152,7 @@ } } -.placeholder--feed { +.placeholder--feedMy { background: #171032; .placeholderCover { @@ -190,6 +214,11 @@ @include font-size(1.6rem); gap: 4rem; + @include media-breakpoint-down(sm) { + flex-direction: column; + gap: 1.4rem; + } + a { border: none !important; padding-left: 2.6rem; diff --git a/src/components/Feed/Placeholder/Placeholder.tsx b/src/components/Feed/Placeholder/Placeholder.tsx index 94fa247a..a0b7dce2 100644 --- a/src/components/Feed/Placeholder/Placeholder.tsx +++ b/src/components/Feed/Placeholder/Placeholder.tsx @@ -16,7 +16,7 @@ export const Placeholder = (props: PlaceholderProps) => { const { author } = useSession() const data = { - feed: { + feedMy: { image: 'placeholder-feed.webp', header: t('Feed settings'), text: t('Placeholder feed'), diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index c5a44e98..b8414e40 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -260,28 +260,32 @@ export const AuthorView = (props: Props) => {
-
- -
+ +
+ +
+
-
-
-
-
    - - {(comment) => ( - handleDeleteComment(id)} - /> - )} - -
+ +
+
+
+
    + + {(comment) => ( + handleDeleteComment(id)} + /> + )} + +
+
-
+ {
- } - > + + + + +
  • { - - - + From 22f0c9052dfb0876b92c6d055283c74f933da095 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 01:04:18 +0300 Subject: [PATCH 13/20] auth-errors-fix --- public/locales/en/translation.json | 3 ++- public/locales/ru/translation.json | 3 ++- src/components/Nav/AuthModal/LoginForm.tsx | 10 +++------- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index dea79bdb..72071723 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -417,6 +417,7 @@ "Username": "Username", "Userpic": "Userpic", "Users": "Users", + "User was not found": "User was not found", "Video format not supported": "Video format not supported", "Video": "Video", "Views": "Views", @@ -540,4 +541,4 @@ "Incorrect old password": "Incorrect old password", "Repeat new password": "Repeat new password", "Incorrect new password confirm": "Incorrect new password confirm" -} +} \ No newline at end of file diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index f9596f64..84006a9b 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -549,6 +549,7 @@ "topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования", "topics": "темы", "user already exist": "пользователь уже существует", + "User was not found": "Пользователь не найден", "verified": "уже подтверждён", "video": "видео", "view": "просмотр", @@ -567,4 +568,4 @@ "Incorrect old password": "Старый пароль не верен", "Repeat new password": "Повторите новый пароль", "Incorrect new password confirm": "Неверное подтверждение нового пароля" -} +} \ No newline at end of file diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 25867cec..b33dabdc 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -96,18 +96,14 @@ export const LoginForm = () => { try { const { errors } = await signIn({ email: email(), password: password() }) if (errors?.length > 0) { - console.debug('[signIn] errors:', errors) - if ( - errors.some( - (error) => error.message.includes('bad user credentials') || error.message.includes('user not'), - ) - ) { + console.error('[signIn errors]', errors) + if (errors.some((error) => error.message.includes('user has not signed up email & password'))) { setValidationErrors((prev) => ({ ...prev, password: t('Something went wrong, check email and password'), })) } else if (errors.some((error) => error.message.includes('user not found'))) { - setSubmitError('Пользователь не найден') + setSubmitError(t('User was not found')) } else if (errors.some((error) => error.message.includes('email not verified'))) { setSubmitError(
    From 59eaf3837da216a1b8ac7d13e00b0c80c4f6b2b1 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 01:14:28 +0300 Subject: [PATCH 14/20] handle-auth-errors --- src/components/Nav/AuthModal/LoginForm.tsx | 29 ++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index b33dabdc..dc5ffe54 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -96,27 +96,30 @@ export const LoginForm = () => { try { const { errors } = await signIn({ email: email(), password: password() }) if (errors?.length > 0) { - console.error('[signIn errors]', errors) - if (errors.some((error) => error.message.includes('user has not signed up email & password'))) { - setValidationErrors((prev) => ({ - ...prev, - password: t('Something went wrong, check email and password'), - })) - } else if (errors.some((error) => error.message.includes('user not found'))) { - setSubmitError(t('User was not found')) - } else if (errors.some((error) => error.message.includes('email not verified'))) { + console.warn('[signIn] errors: ', errors) + let msg = '' + if (errors.some((error) => error.message === 'user has not signed up email & password')) { + const password = t('Something went wrong, check email and password') + setValidationErrors((prev) => ({ ...prev, password })) + } else if (errors.some((error) => error.message === 'user not found')) { + msg = t('User was not found') + } else if (errors.some((error) => error.message === 'email not verified')) { + msg = t('This email is not verified') + } else { + msg = t('Error', errors[0].message) + } + + msg && setSubmitError(
    - {t('This email is not verified')} + {msg} {'. '} {t('Send link again')}
    , ) - } else { - setSubmitError(t('Error', errors[0].message)) - } + return } hideModal() From 38899ad8cb3aec26da62899a18f4c7402419b5a1 Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 01:18:55 +0300 Subject: [PATCH 15/20] login-validations-fixes --- src/components/Nav/AuthModal/LoginForm.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index dc5ffe54..dc57565b 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -97,28 +97,27 @@ export const LoginForm = () => { const { errors } = await signIn({ email: email(), password: password() }) if (errors?.length > 0) { console.warn('[signIn] errors: ', errors) - let msg = '' + if (errors.some((error) => error.message === 'user has not signed up email & password')) { const password = t('Something went wrong, check email and password') setValidationErrors((prev) => ({ ...prev, password })) } else if (errors.some((error) => error.message === 'user not found')) { - msg = t('User was not found') + const email = t('User was not found') + setValidationErrors((prev) => ({ ...prev, email })) } else if (errors.some((error) => error.message === 'email not verified')) { - msg = t('This email is not verified') + const email = t('This email is not verified') + setValidationErrors((prev) => ({ ...prev, email })) } else { - msg = t('Error', errors[0].message) - } - - msg && setSubmitError(
    - {msg} + {t('Error', errors[0].message)} {'. '} {t('Send link again')}
    , ) + } return } From 319136474e710117771e6e69e0ae1a5546a02b1b Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Sun, 19 May 2024 01:38:56 +0300 Subject: [PATCH 16/20] Table of contents minor fixes --- public/locales/en/translation.json | 4 ++-- public/locales/ru/translation.json | 4 ++-- src/components/TableOfContents/TableOfContents.module.scss | 2 +- src/components/TableOfContents/TableOfContents.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 72071723..29fed7a8 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -91,6 +91,7 @@ "Community Principles": "Community Principles", "Community values and rules of engagement for the open editorial team": "Community values and rules of engagement for the open editorial team", "Confirm": "Confirm", + "Contents": "Contents", "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom", "Cooperate": "Cooperate", "Copy link": "Copy link", @@ -470,7 +471,6 @@ "cancel": "cancel", "collections": "collections", "community": "community", - "contents": "contents", "delimiter": "delimiter", "discussion": "Discours", "dogma keywords": "Discours.io, dogma, editorial principles, code of ethics, journalism, community", @@ -541,4 +541,4 @@ "Incorrect old password": "Incorrect old password", "Repeat new password": "Repeat new password", "Incorrect new password confirm": "Incorrect new password confirm" -} \ No newline at end of file +} diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 84006a9b..91134e24 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -95,6 +95,7 @@ "Community Principles": "Принципы сообщества", "Community values and rules of engagement for the open editorial team": "Ценности сообщества и правила взаимодействия открытой редакции", "Confirm": "Подтвердить", + "Contents": "Оглавление", "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Внесите вклад в свободный самиздат. Поддержите Дискурс — независимое некоммерческое издание, которое работает только для вас. Станьте опорой открытой редакции", "Cooperate": "Соучаствовать", "Copy link": "Скопировать ссылку", @@ -492,7 +493,6 @@ "cancel": "отменить", "collections": "коллекции", "community": "сообщество", - "contents": "оглавление", "create_chat": "Создать чат", "create_group": "Создать группу", "delimiter": "разделитель", @@ -568,4 +568,4 @@ "Incorrect old password": "Старый пароль не верен", "Repeat new password": "Повторите новый пароль", "Incorrect new password confirm": "Неверное подтверждение нового пароля" -} \ No newline at end of file +} diff --git a/src/components/TableOfContents/TableOfContents.module.scss b/src/components/TableOfContents/TableOfContents.module.scss index 2e5fe4ac..8433e44d 100644 --- a/src/components/TableOfContents/TableOfContents.module.scss +++ b/src/components/TableOfContents/TableOfContents.module.scss @@ -157,7 +157,7 @@ color: #000; font-size: 14px; font-style: normal; - font-weight: 400; + font-weight: 500; line-height: 1.8rem; text-align: left; vertical-align: bottom; diff --git a/src/components/TableOfContents/TableOfContents.tsx b/src/components/TableOfContents/TableOfContents.tsx index 322aa925..d0a2de05 100644 --- a/src/components/TableOfContents/TableOfContents.tsx +++ b/src/components/TableOfContents/TableOfContents.tsx @@ -86,7 +86,7 @@ export const TableOfContents = (props: Props) => {
    -

    {t('contents')}

    +

    {t('Contents')}

      From 8f330ab914a835409b4f2a695aade286229fefee Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 01:41:50 +0300 Subject: [PATCH 17/20] multierror --- src/components/Nav/AuthModal/LoginForm.tsx | 51 ++++++++++++---------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index dc57565b..925c8864 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -97,28 +97,35 @@ export const LoginForm = () => { const { errors } = await signIn({ email: email(), password: password() }) if (errors?.length > 0) { console.warn('[signIn] errors: ', errors) - - if (errors.some((error) => error.message === 'user has not signed up email & password')) { - const password = t('Something went wrong, check email and password') - setValidationErrors((prev) => ({ ...prev, password })) - } else if (errors.some((error) => error.message === 'user not found')) { - const email = t('User was not found') - setValidationErrors((prev) => ({ ...prev, email })) - } else if (errors.some((error) => error.message === 'email not verified')) { - const email = t('This email is not verified') - setValidationErrors((prev) => ({ ...prev, email })) - } else { - setSubmitError( -
      - {t('Error', errors[0].message)} - {'. '} - - {t('Send link again')} - -
      , - ) - } - + errors.forEach((error) => { + switch (error.message) { + case 'user has not signed up email & password': { + setValidationErrors((prev) => ({ + ...prev, + password: t('Something went wrong, check email and password'), + })) + break + } + case 'user not found': { + setValidationErrors((prev) => ({ ...prev, email: t('User was not found') })) + break + } + case 'email not verified': { + setValidationErrors((prev) => ({ ...prev, email: t('This email is not verified') })) + break + } + default: + setSubmitError( +
      + {t('Error', errors[0].message)} + {'. '} + + {t('Send link again')} + +
      , + ) + } + }) return } hideModal() From 9b76a52430980d3e5424655d2b7f07c6618a8bcf Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 19 May 2024 02:22:19 +0300 Subject: [PATCH 18/20] editor-autosave-fix --- src/components/Views/EditView/EditView.tsx | 63 +++++++++++----------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 1e8650b1..2148a3e4 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -2,7 +2,7 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' -import { throttle } from 'throttle-debounce' +import { debounce } from 'throttle-debounce' import { ShoutForm, useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' @@ -42,9 +42,8 @@ export const EMPTY_TOPIC: Topic = { slug: '', } -const THROTTLING_INTERVAL = 2000 -const AUTO_SAVE_INTERVAL = 5000 -const AUTO_SAVE_DELAY = 5000 +const AUTO_SAVE_DELAY = 3000 + const handleScrollTopButtonClick = (e) => { e.preventDefault() window.scrollTo({ @@ -104,6 +103,8 @@ export const EditView = (props: Props) => { return JSON.parse(form.media || '[]') }) + const [hasChanges, setHasChanges] = createSignal(false) + onMount(() => { const handleScroll = () => { setIsScrolled(window.scrollY > 0) @@ -113,7 +114,7 @@ export const EditView = (props: Props) => { onCleanup(() => { window.removeEventListener('scroll', handleScroll) }) - // eslint-disable-next-line unicorn/consistent-function-scoping + const handleBeforeUnload = (event) => { if (!deepEqual(prevForm, form)) { event.returnValue = t( @@ -127,8 +128,8 @@ export const EditView = (props: Props) => { }) const handleTitleInputChange = (value: string) => { - setForm('title', value) - setForm('slug', slugify(value)) + handleInputChange('title', value) + handleInputChange('slug', slugify(value)) if (value) { setFormErrors('title', '') } @@ -136,21 +137,21 @@ export const EditView = (props: Props) => { const handleAddMedia = (data) => { const newMedia = [...mediaItems(), ...data] - setForm('media', JSON.stringify(newMedia)) + handleInputChange('media', JSON.stringify(newMedia)) } const handleSortedMedia = (data) => { - setForm('media', JSON.stringify(data)) + handleInputChange('media', JSON.stringify(data)) } const handleMediaDelete = (index) => { const copy = [...mediaItems()] copy.splice(index, 1) - setForm('media', JSON.stringify(copy)) + handleInputChange('media', JSON.stringify(copy)) } const handleMediaChange = (index, value) => { const updated = mediaItems().map((item, idx) => (idx === index ? value : item)) - setForm('media', JSON.stringify(updated)) + handleInputChange('media', JSON.stringify(updated)) } const [baseAudioFields, setBaseAudioFields] = createSignal({ @@ -162,7 +163,7 @@ export const EditView = (props: Props) => { const handleBaseFieldsChange = (key, value) => { if (mediaItems().length > 0) { const updated = mediaItems().map((media) => ({ ...media, [key]: value })) - setForm('media', JSON.stringify(updated)) + handleInputChange('media', JSON.stringify(updated)) } else { setBaseAudioFields({ ...baseAudioFields(), [key]: value }) } @@ -182,34 +183,32 @@ export const EditView = (props: Props) => { } } - let autoSaveTimeOutId: number | string | NodeJS.Timeout - const autoSave = async () => { - const hasChanges = !deepEqual(form, prevForm) - const hasTopic = Boolean(form.mainTopic) - if (hasChanges || hasTopic) { + console.log('autoSave called') + if (hasChanges()) { console.debug('saving draft', form) setSaving(true) saveDraftToLocalStorage(form) await saveDraft(form) setPrevForm(clone(form)) - setTimeout(() => setSaving(false), AUTO_SAVE_DELAY) + setSaving(false) + setHasChanges(false) } } - // Throttle the autoSave function - const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave) + const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, autoSave) - const autoSaveRecursive = () => { - autoSaveTimeOutId = setTimeout(() => { - throttledAutoSave() - autoSaveRecursive() - }, AUTO_SAVE_INTERVAL) + const handleInputChange = (key, value) => { + console.log(`[handleInputChange] ${key}: ${value}`) + setForm(key, value) + setHasChanges(true) + debouncedAutoSave() } onMount(() => { - autoSaveRecursive() - onCleanup(() => clearTimeout(autoSaveTimeOutId)) + onCleanup(() => { + debouncedAutoSave.cancel() + }) }) const showSubtitleInput = () => { @@ -310,7 +309,7 @@ export const EditView = (props: Props) => { subtitleInput.current = el }} allowEnterKey={false} - value={(value) => setForm('subtitle', value || '')} + value={(value) => handleInputChange('subtitle', value || '')} class={styles.subtitleInput} placeholder={t('Subheader')} initialValue={form.subtitle || ''} @@ -324,7 +323,7 @@ export const EditView = (props: Props) => { smallHeight={true} placeholder={t('A short introduction to keep the reader interested')} initialContent={form.lead} - onChange={(value) => setForm('lead', value)} + onChange={(value) => handleInputChange('lead', value)} /> @@ -345,7 +344,7 @@ export const EditView = (props: Props) => { } isMultiply={false} fileType={'image'} - onUpload={(val) => setForm('coverImageUrl', val[0].url)} + onUpload={(val) => handleInputChange('coverImageUrl', val[0].url)} /> } > @@ -362,7 +361,7 @@ export const EditView = (props: Props) => {
      setForm('coverImageUrl', null)} + onClick={() => handleInputChange('coverImageUrl', null)} >
      @@ -408,7 +407,7 @@ export const EditView = (props: Props) => { setForm('body', body)} + onChange={(body) => handleInputChange('body', body)} />
    From 0631ecf8c95ee523202d17720275db6763599863 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Mon, 20 May 2024 23:51:33 +0300 Subject: [PATCH 19/20] Fixed line breaks --- public/locales/ru/translation.json | 6 +++--- src/components/Feed/Placeholder/Placeholder.module.scss | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 2987f1f4..c0fcccd9 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -298,9 +298,9 @@ "Paste Embed code": "Вставьте embed код", "Personal": "Личные", "Pin": "Закрепить", - "Placeholder feed": "Подпишитесь на любимые темы, авторов и сообщества — моментально узнавайте о новых публикациях и обсуждениях", - "Placeholder feedCollaborations": "На платформе можно писать материалы вместе. Здесь появятся публикации, в которые вы внесли вклад", - "Placeholder feedDiscussions": "Дискурс — свободная платформа для осмысленного общения. Здесь появятся все ваши реплики, чтобы в любой момент вернуться к диалогу", + "Placeholder feed": "Подпишитесь на любимые темы, авторов и сообщества —
    моментально узнавайте о новых публикациях и обсуждениях", + "Placeholder feedCollaborations": "На платформе можно писать материалы вместе.
    Здесь появятся публикации, в которые вы внесли вклад", + "Placeholder feedDiscussions": "Дискурс — свободная платформа для осмысленного общения.
    Здесь появятся все ваши реплики, чтобы в любой момент вернуться к диалогу", "Platform Guide": "Гид по дискурсу", "Please check your email address": "Пожалуйста, проверьте введенный адрес почты", "Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля", diff --git a/src/components/Feed/Placeholder/Placeholder.module.scss b/src/components/Feed/Placeholder/Placeholder.module.scss index 42b4e765..efb2362f 100644 --- a/src/components/Feed/Placeholder/Placeholder.module.scss +++ b/src/components/Feed/Placeholder/Placeholder.module.scss @@ -139,6 +139,12 @@ .placeholderContent { padding: 1.6rem; + + @include media-breakpoint-down(lg) { + br { + display: none; + } + } } .placeholder--feedMy, From 2e6e1abad7d07d15c11a061a6aba2ee4e06c18c8 Mon Sep 17 00:00:00 2001 From: kvakazyambra Date: Tue, 21 May 2024 00:26:55 +0300 Subject: [PATCH 20/20] Fixed banners style --- .../Feed/Placeholder/Placeholder.module.scss | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/components/Feed/Placeholder/Placeholder.module.scss b/src/components/Feed/Placeholder/Placeholder.module.scss index efb2362f..02942072 100644 --- a/src/components/Feed/Placeholder/Placeholder.module.scss +++ b/src/components/Feed/Placeholder/Placeholder.module.scss @@ -1,13 +1,13 @@ .placeholder { border-radius: 2.2rem; display: flex; - @include font-size(1.4rem); + font-size: 1.4rem; font-weight: 500; overflow: hidden; position: relative; h3 { - @include font-size(2.4rem); + font-size: 2.4rem; } button, @@ -68,28 +68,34 @@ .placeholder--profile-mode { min-height: 40rem; - @include media-breakpoint-down(md) { + @include media-breakpoint-down(lg) { display: block; } - @include media-breakpoint-up(md) { - min-height: 28rem; + @include media-breakpoint-up(lg) { + max-height: 30rem; + min-height: auto; } .placeholderCover { padding: 1.6rem; - @include media-breakpoint-up(md) { - flex: 0 45rem; - min-width: 50%; + @include media-breakpoint-up(lg) { + //flex: 0 50%; + //min-width: 50%; order: 2; position: static; - width: 45rem; } img { - height: auto; + height: 100%; + object-fit: contain; width: 100%; + //width: auto; + + @include media-breakpoint-up(lg) { + object-position: right; + } } } @@ -97,9 +103,19 @@ display: flex; flex-direction: column; justify-content: space-between; - @include font-size(2rem); + font-size: 1.4rem; line-height: 1.2; - padding: 3rem; + min-width: 60%; + padding: 0 2rem 2rem; + + @include media-breakpoint-up(md) { + font-size: 1.6rem; + padding: 3rem; + } + + @include media-breakpoint-up(lg) { + font-size: 2rem; + } } h3 { @@ -108,13 +124,14 @@ .button { background: var(--background-color-invert); - color: var(--default-color-invert); bottom: 2rem; + color: var(--default-color-invert); + font-size: 1.6rem; left: 2rem; right: 2rem; width: 100%; - @include media-breakpoint-up(md) { + @include media-breakpoint-up(lg) { left: auto; position: absolute; width: auto;