diff --git a/.gitignore b/.gitignore index 0dc8807d..52b0bfb0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ bun.lockb /blob-report/ /playwright/.cache/ /plawright-report/ +target .venv diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 96e02afe..3249be2a 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -18,6 +18,7 @@ "Add signature": "Add signature", "Add subtitle": "Add subtitle", "Add url": "Add url", + "try": "попробуйте", "Add": "Add", "Address on Discours": "Address on Discours", "Album name": "Название aльбома", @@ -144,7 +145,6 @@ "Enter your new password": "Enter your new password", "Enter": "Enter", "Error": "Error", - "Please give us your email address": "Please provide us your email address to get the password reset link", "Experience": "Experience", "FAQ": "Tips and suggestions", "Favorite topics": "Favorite topics", @@ -254,7 +254,6 @@ "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", "Notifications": "Notifications", - "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password": "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password", "Or paste a link to an image": "Or paste a link to an image", "Ordered list": "Ordered list", "Our regular contributor": "Our regular contributor", @@ -323,7 +322,7 @@ "Self-publishing exists thanks to the help of wonderful people from all over the world. Thank you!": "Samizdat exists thanks to the help of wonderful people from all over the world. Thank you!", "Send link again": "Send link again", "Send": "Send", - "Set the new password": "Set the new password", + "Forgot password?": "Forgot password?", "Settings": "Settings", "Share publication": "Share publication", "Share": "Share", @@ -380,6 +379,7 @@ "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?", "This comment has not yet been rated": "This comment has not yet been rated", + "This content is not published yet": "This content is not published yet", "This email is": "This email is", "This email is not verified": "This email is not verified", "This email is verified": "This email is verified", @@ -525,5 +525,8 @@ "video": "video", "view": "view", "viewsWithCount": "{count} {count, plural, one {view} other {views}}", - "yesterday": "yesterday" + "yesterday": "yesterday", + "Failed to delete comment": "Failed to delete comment", + "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" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index ef1e6b8c..d4e5bfcd 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -149,8 +149,8 @@ "Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации", "Enter your new password": "Введите новый пароль", "Enter": "Войти", + "This content is not published yet": "Содержимое ещё не опубликовано", "Error": "Ошибка", - "Please give us your email address": "Пожалуйста, укажите свою почту, чтобы получить ссылку для сброса пароля", "Experience": "Личный опыт", "FAQ": "Советы и предложения", "Favorite topics": "Избранные темы", @@ -266,7 +266,6 @@ "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", "Notifications": "Уведомления", - "Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password": "Теперь можете ввести новый пароль, он должен содержать минимум 8 символов и не совпадать с предыдущим паролем", "Or paste a link to an image": "Или вставьте ссылку на изображение", "Ordered list": "Нумерованный список", "Our regular contributor": "Наш постоянный автор", @@ -287,7 +286,7 @@ "Pin": "Закрепить", "Platform Guide": "Гид по дискурсу", "Please check your email address": "Пожалуйста, проверьте введенный адрес почты", - "Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте ваш адрес почты, мы отправили ссылку для сброса пароля", + "Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля", "Please confirm your email to finish": "Подтвердите почту и действие совершится", "Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте", "Please enter email": "Пожалуйста, введите почту", @@ -328,7 +327,7 @@ "Reports": "Репортажи", "Required": "Поле обязательно для заполнения", "Resend code": "Выслать подтверждение", - "Set the new password": "Задать новый пароль", + "Forgot password?": "Забыли пароль?", "Rules of the journal Discours": "Правила журнала Дискурс", "Save draft": "Сохранить черновик", "Save settings": "Сохранить настройки", @@ -404,6 +403,7 @@ "This email is": "Этот email", "This email is not verified": "Этот email не подтвержден", "This email is verified": "Этот email подтвержден", + "try": "попробуйте", "This email is registered": "Этот email уже зарегистрирован", "This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.", "This month": "За месяц", @@ -531,6 +531,7 @@ "repeat": "повторить", "resend confirmation link": "отправить ссылку ещё раз", "shout": "пост", + "shout not found": "публикация не найдена", "shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}", "sign in": "войти", "sign up or sign in": "зарегистрироваться или войти", @@ -551,5 +552,8 @@ "video": "видео", "view": "просмотр", "viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}", - "yesterday": "вчера" + "yesterday": "вчера", + "Failed to delete comment": "Не удалось удалить комментарий", + "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", + "Restore password": "Восстановить пароль" } diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index 31fc51be..170810d9 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -64,14 +64,19 @@ export const Comment = (props: Props) => { }) if (isConfirmed) { - await deleteReaction(props.comment.id) - // TODO: Учесть то что deleteReaction может вернуть error - if (props.onDelete) { + const { error } = await deleteReaction(props.comment.id) + const notificationType = error ? 'error' : 'success' + const notificationMessage = error + ? t('Failed to delete comment') + : t('Comment successfully deleted') + await showSnackbar({ type: notificationType, body: notificationMessage }) + + if (!error && props.onDelete) { props.onDelete(props.comment.id) } - await showSnackbar({ body: t('Comment successfully deleted') }) } } catch (error) { + await showSnackbar({ body: 'error' }) console.error('[deleteReaction]', error) } } diff --git a/src/components/Article/CommentDate/CommentDate.module.scss b/src/components/Article/CommentDate/CommentDate.module.scss index 89d90585..50cf7d57 100644 --- a/src/components/Article/CommentDate/CommentDate.module.scss +++ b/src/components/Article/CommentDate/CommentDate.module.scss @@ -2,8 +2,6 @@ @include font-size(1.2rem); color: var(--secondary-color); - - // align-self: center; display: flex; align-items: flex-start; justify-content: flex-start; diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 024cf1a0..ef5a4b80 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -141,7 +141,7 @@ export const FullArticle = (props: Props) => { const media = createMemo(() => { try { - return JSON.parse(props.article.media) + return JSON.parse(props.article?.media || '[]') } catch { return [] } diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index cd303717..d55ed55f 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -67,19 +67,19 @@ const getTitleAndSubtitle = ( subtitle: string } => { let title = article.title - let subtitle = article.subtitle + let subtitle: string = article.subtitle || '' if (!subtitle) { - let tt = article.title?.split('. ') || [] + let titleParts = article.title?.split('. ') || [] - if (tt?.length === 1) { - tt = article.title?.split(/{!|\?|:|;}\s/) || [] + if (titleParts?.length === 1) { + titleParts = article.title?.split(/{!|\?|:|;}\s/) || [] } - if (tt && tt.length > 1) { - const sep = article.title?.replace(tt[0], '').split(' ', 1)[0] - title = tt[0] + (sep === '.' || sep === ':' ? '' : sep) - subtitle = capitalize(article.title?.replace(tt[0] + sep, ''), true) + if (titleParts && titleParts.length > 1) { + const sep = article.title?.replace(titleParts[0], '').split(' ', 1)[0] + title = titleParts[0] + (sep === '.' || sep === ':' ? '' : sep) + subtitle = capitalize(article.title?.replace(titleParts[0] + sep, ''), true) || '' } } @@ -117,7 +117,7 @@ export const ArticleCard = (props: ArticleCardProps) => { const { title, subtitle } = getTitleAndSubtitle(props.article) const formattedDate = createMemo(() => - props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '', + props.article?.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '', ) const canEdit = createMemo( @@ -135,6 +135,7 @@ export const ArticleCard = (props: ArticleCardProps) => { scrollTo: 'comments', }) } + return (
{ [aspectRatio()]: props.withAspectRatio, })} > + {/* Cover Image */} + {/* Cover Image Container */}
{
+ + {/* Shout Card Content */}
+ {/* Shout Card Icon */} {
+ {/* Main Topic */} { /> + {/* Title and Subtitle */}
{
+ + {/* Details */} + {/* Author and Date */}
- {(a: Author) => { - return ( - - ) - }} + {(a: Author) => ( + + )}
@@ -248,6 +257,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
+ + {/* Description */}
diff --git a/src/components/Inbox/DialogAvatar.tsx b/src/components/Inbox/DialogAvatar.tsx index 875cdc98..d34067d6 100644 --- a/src/components/Inbox/DialogAvatar.tsx +++ b/src/components/Inbox/DialogAvatar.tsx @@ -51,7 +51,15 @@ const DialogAvatar = (props: Props) => { {nameFirstLetter()}}>
diff --git a/src/components/Nav/AuthModal/AuthModal.module.scss b/src/components/Nav/AuthModal/AuthModal.module.scss index 23bd6aff..2d4cbc98 100644 --- a/src/components/Nav/AuthModal/AuthModal.module.scss +++ b/src/components/Nav/AuthModal/AuthModal.module.scss @@ -1,5 +1,5 @@ .view { - background: #fff; + background: var(--background-color); min-height: 550px; position: relative; justify-content: center; @@ -154,17 +154,6 @@ margin-bottom: 1em; } -.authInfo { - font-weight: 400; - font-size: smaller; - margin-top: -2em; - position: absolute; - - .warn { - color: #a00; - } -} - .authForm { display: flex; flex: 1; @@ -221,3 +210,7 @@ line-height: 24px; margin-bottom: 52px; } + +.submitError { + margin: -1rem 0 -2rem; +} diff --git a/src/components/Nav/AuthModal/ChangePasswordForm.tsx b/src/components/Nav/AuthModal/ChangePasswordForm.tsx index 2fa9c3a9..a317e834 100644 --- a/src/components/Nav/AuthModal/ChangePasswordForm.tsx +++ b/src/components/Nav/AuthModal/ChangePasswordForm.tsx @@ -33,7 +33,7 @@ export const ChangePasswordForm = () => { event.preventDefault() setIsSubmitting(true) if (newPassword()) { - await changePassword(newPassword(), searchParams()?.token) + changePassword(newPassword(), searchParams()?.token) setTimeout(() => { setIsSubmitting(false) setIsSuccess(true) @@ -60,11 +60,6 @@ export const ChangePasswordForm = () => { >

{t('Enter a new password')}

-
- {t( - 'Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password', - )} -
{validationErrors().password}
diff --git a/src/components/Nav/AuthModal/EmailConfirm.module.scss b/src/components/Nav/AuthModal/EmailConfirm.module.scss deleted file mode 100644 index 323b5384..00000000 --- a/src/components/Nav/AuthModal/EmailConfirm.module.scss +++ /dev/null @@ -1,13 +0,0 @@ -.title { - font-size: 26px; - line-height: 32px; - font-weight: 700; - color: #141414; - margin-bottom: 16px; -} - -.text { - font-size: 15px; - line-height: 24px; - margin-bottom: 52px; -} diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 1bd88086..2a7367bc 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -17,19 +17,20 @@ export const EmailConfirm = () => { const [emailConfirmed, setEmailConfirmed] = createSignal(false) createEffect(() => { - const e = session()?.user?.email - const v = session()?.user?.email_verified - if (e) { - setEmail(e.toLowerCase()) - if (v) setEmailConfirmed(v) + const email = session()?.user?.email + const isVerified = session()?.user?.email_verified + + if (email) { + setEmail(email.toLowerCase()) + if (isVerified) setEmailConfirmed(isVerified) if (authError()) { changeSearchParams({}, true) } } - }) - createEffect(() => { - if (authError()) console.debug('[AuthModal.EmailConfirm] auth error:', authError()) + if (authError()) { + console.debug('[AuthModal.EmailConfirm] auth error:', authError()) + } }) return ( diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index b708d32b..4ea5a5f6 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -1,7 +1,7 @@ import type { AuthModalSearchParams } from './types' import { clsx } from 'clsx' -import { Show, createSignal } from 'solid-js' +import { JSX, Show, createEffect, createSignal } from 'solid-js' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' @@ -27,12 +27,11 @@ type ValidationErrors = Partial> export const LoginForm = () => { const { changeSearchParams } = useRouter() const { t } = useLocalize() - const [submitError, setSubmitError] = createSignal('') + const [submitError, setSubmitError] = createSignal() const [isSubmitting, setIsSubmitting] = createSignal(false) const [password, setPassword] = createSignal('') const [validationErrors, setValidationErrors] = createSignal({}) - // TODO: better solution for interactive error messages - const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) + const [isLinkSent, setIsLinkSent] = createSignal(false) const authFormRef: { current: HTMLFormElement } = { current: null } const { showSnackbar } = useSnackbar() @@ -52,43 +51,43 @@ export const LoginForm = () => { event.preventDefault() setIsLinkSent(true) - setIsEmailNotConfirmed(false) - setSubmitError('') - changeSearchParams({ mode: 'send-reset-link' }) - // NOTE: temporary solution, needs logic in authorizer - /* FIXME: - const { authorizer } = useSession() - const result = await authorizer().verifyEmail({ token }) - if (!result) setSubmitError('cant sign send link') - */ + setSubmitError() + changeSearchParams({ mode: 'send-confirm-email' }) } + const preSendValidate = async (value: string, type: 'email' | 'password'): Promise => { + if (type === 'email') { + if (value === '' || !validateEmail(value)) { + setValidationErrors((prev) => ({ + ...prev, + email: t('Invalid email'), + })) + return false + } + } else if (type === 'password') { + if (value === '') { + setValidationErrors((prev) => ({ + ...prev, + password: t('Please enter password'), + })) + return false + } + } + return true + } const handleSubmit = async (event: Event) => { event.preventDefault() + await preSendValidate(email(), 'email') + await preSendValidate(password(), 'password') + setIsLinkSent(false) - setIsEmailNotConfirmed(false) - setSubmitError('') - - const newValidationErrors: ValidationErrors = {} - - const validateAndSetError = (field, message) => { - if (!field()) { - newValidationErrors[field.name] = t(message) - } - } - - validateAndSetError(email, 'Please enter email') - validateAndSetError(() => validateEmail(email()), 'Invalid email') - validateAndSetError(password, 'Please enter password') - - if (Object.keys(newValidationErrors).length > 0) { - setValidationErrors(newValidationErrors) + setSubmitError() + if (Object.keys(validationErrors()).length > 0) { authFormRef.current - .querySelector(`input[name="${Object.keys(newValidationErrors)[0]}"]`) + .querySelector(`input[name="${Object.keys(validationErrors())[0]}"]`) ?.focus() - return } @@ -96,14 +95,27 @@ export const LoginForm = () => { try { const { errors } = await signIn({ email: email(), password: password() }) + console.error('[signIn errors]', errors) if (errors?.length > 0) { if (errors.some((error) => error.message.includes('bad user credentials'))) { setValidationErrors((prev) => ({ ...prev, password: t('Something went wrong, check email and password'), })) + } else if (errors.some((error) => error.message.includes('user not found'))) { + setSubmitError('Пользователь не найден') + } else if (errors.some((error) => error.message.includes('email not verified'))) { + setSubmitError( +
+ {t('This email is not verified')} + {'. '} + + {t('Send link again')} + +
, + ) } else { - setSubmitError(t('Error')) + setSubmitError(t('Error', errors[0].message)) } return } @@ -121,19 +133,6 @@ export const LoginForm = () => {
(authFormRef.current = el)}>
- -
-
{submitError()}
- - - {t('Send link again')} - - -
-
- -
{t('Link sent, check your email')}
-
{
- handlePasswordInput(value)} /> - -
- {validationErrors().password} -
+ handlePasswordInput(value)} + /> + + +
{submitError()}
@@ -175,7 +177,7 @@ export const LoginForm = () => { }) } > - {t('Set the new password')} + {t('Forgot password?')}
diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss index 7faae657..f050d700 100644 --- a/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss @@ -31,11 +31,11 @@ } /* Red/500 */ - color: #d00820; + color: orange; a { - color: #d00820; - border-color: #d00820; + color: orange; + border-color: orange; &:hover { color: var(--default-color-invert); diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx index c2e0e3bc..bf7f00b1 100644 --- a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -11,21 +11,23 @@ type Props = { disabled?: boolean placeholder?: string errorMessage?: (error: string) => void + setError?: string onInput: (value: string) => void + onBlur?: (value: string) => void variant?: 'login' | 'registration' disableAutocomplete?: boolean } +const minLength = 8 +const hasNumber = /\d/ +const hasSpecial = /[!#$%&*@^]/ + export const PasswordField = (props: Props) => { const { t } = useLocalize() const [showPassword, setShowPassword] = createSignal(false) const [error, setError] = createSignal() const validatePassword = (passwordToCheck) => { - const minLength = 8 - const hasNumber = /\d/ - const hasSpecial = /[!#$%&*@^]/ - if (passwordToCheck.length < minLength) { return t('Password should be at least 8 characters') } @@ -35,11 +37,17 @@ export const PasswordField = (props: Props) => { if (!hasSpecial.test(passwordToCheck)) { return t('Password should contain at least one special character: !@#$%^&*') } - return null } - const handleInputChange = (value) => { + const handleInputBlur = (value: string) => { + if (props.variant === 'login') { + return props.onBlur(value) + } + if (value.length < 1) { + return + } + props.onInput(value) const errorValue = validatePassword(value) if (errorValue) { @@ -58,14 +66,13 @@ export const PasswordField = (props: Props) => { { defer: true }, ), ) + createEffect(() => { + setError(props.setError) + }) return (
-
+
{ autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'} type={showPassword() ? 'text' : 'password'} placeholder={props.placeholder || t('Password')} - onInput={(event) => handleInputChange(event.currentTarget.value)} + onBlur={(event) => handleInputBlur(event.currentTarget.value)} /> - -
{error()}
+ +
+ {error()} +
diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index b40cfa8b..56f272ac 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -28,10 +28,6 @@ type FormFields = { type ValidationErrors = Partial> -const handleEmailInput = (newEmail: string) => { - setEmail(newEmail.toLowerCase()) -} - export const RegisterForm = () => { const { changeSearchParams } = useRouter() const { t } = useLocalize() @@ -137,7 +133,8 @@ export const RegisterForm = () => { setValidationErrors((prev) => ({ email: ( <> - {t('This email is verified')}. {t('You can')}{' '} + {t('This email is registered')}. {t('try')} + {', '} changeSearchParams({ mode: 'login' })}> {t('enter')} @@ -150,9 +147,10 @@ export const RegisterForm = () => { ...prev, email: ( <> - {t('This email is registered')}. {t('You can')}{' '} + {t('This email is registered')} + {'. '} changeSearchParams({ mode: 'send-reset-link' })}> - {t('Set the new password').toLocaleLowerCase()} + {t('Set the new password')} ), @@ -172,17 +170,18 @@ export const RegisterForm = () => { } } + const handleEmailInput = (newEmail: string) => { + setEmailStatus('') + setValidationErrors({}) + setEmail(newEmail.toLowerCase()) + } + return ( <> (authFormRef.current = el)}>
- -
-
{submitError()}
-
-
{ disabled={Boolean(emailStatus())} placeholder={t('Full name')} autocomplete="one-time-code" - onInput={(event) => handleNameInput(event.currentTarget.value)} + onChange={(event) => handleNameInput(event.currentTarget.value)} /> @@ -217,16 +216,18 @@ export const RegisterForm = () => { onBlur={handleEmailBlur} /> -
- {validationErrors().email} -
+ +
+ {validationErrors().email} +
+
setPasswordError(err)} - onInput={(value) => setPassword(value)} + errorMessage={(err) => !emailStatus() && setPasswordError(err)} + onInput={(value) => setPassword(emailStatus() ? '' : value)} />
@@ -259,12 +260,14 @@ export const RegisterForm = () => { -
{t('Almost done! Check your email.')}
-
{t("We've sent you a message with a link to enter our website.")}
-
- +
+
{t('Almost done! Check your email.')}
+
{t("We've sent you a message with a link to enter our website.")}
+
+ +
diff --git a/src/components/Nav/AuthModal/SendEmailConfirm.tsx b/src/components/Nav/AuthModal/SendEmailConfirm.tsx new file mode 100644 index 00000000..dd8c01dd --- /dev/null +++ b/src/components/Nav/AuthModal/SendEmailConfirm.tsx @@ -0,0 +1,24 @@ +import { clsx } from 'clsx' +import { useLocalize } from '../../../context/localize' +import { hideModal } from '../../../stores/ui' + +import styles from './AuthModal.module.scss' + +export const SendEmailConfirm = () => { + const { t } = useLocalize() + return ( +
+
{t('Link sent, check your email')}
+
+ +
+
+ ) +} diff --git a/src/components/Nav/AuthModal/SendResetLinkForm.tsx b/src/components/Nav/AuthModal/SendResetLinkForm.tsx index f429a256..e872cbeb 100644 --- a/src/components/Nav/AuthModal/SendResetLinkForm.tsx +++ b/src/components/Nav/AuthModal/SendResetLinkForm.tsx @@ -1,7 +1,7 @@ import type { AuthModalSearchParams } from './types' import { clsx } from 'clsx' -import { JSX, Show, createSignal } from 'solid-js' +import { JSX, Show, createSignal, onMount } from 'solid-js' import { useLocalize } from '../../../context/localize' import { useSession } from '../../../context/session' @@ -72,6 +72,12 @@ export const SendResetLinkForm = () => { } } + onMount(() => { + if (email()) { + console.info('[SendResetLinkForm] email detected') + } + }) + return (
{ ref={(el) => (authFormRef.current = el)} >
-

{t('Set the new password')}

-
{t(message()) || t('Please give us your email address')}
+

{t('Forgot password?')}

+ +
+ {t("It's OK. Just enter your email to receive a link to change your password")} +
+
{ type="email" value={email()} placeholder={t('Email')} - onInput={(event) => handleEmailInput(event.currentTarget.value)} + onChange={(event) => handleEmailInput(event.currentTarget.value)} /> @@ -104,7 +114,7 @@ export const SendResetLinkForm = () => { class={'link'} onClick={() => changeSearchParams({ - mode: 'login', + mode: 'register', }) } > @@ -116,28 +126,31 @@ export const SendResetLinkForm = () => {
{validationErrors().email}
- -
- -
-
- - changeSearchParams({ - mode: 'login', - }) - } - > - {t('I know the password')} - -
+ {t(message())}
}> + <> +
+ +
+
+ + changeSearchParams({ + mode: 'login', + }) + } + > + {t('I know the password')} + +
+ +
) diff --git a/src/components/Nav/AuthModal/SocialProviders.module.scss b/src/components/Nav/AuthModal/SocialProviders/SocialProviders.module.scss similarity index 100% rename from src/components/Nav/AuthModal/SocialProviders.module.scss rename to src/components/Nav/AuthModal/SocialProviders/SocialProviders.module.scss diff --git a/src/components/Nav/AuthModal/SocialProviders.tsx b/src/components/Nav/AuthModal/SocialProviders/SocialProviders.tsx similarity index 70% rename from src/components/Nav/AuthModal/SocialProviders.tsx rename to src/components/Nav/AuthModal/SocialProviders/SocialProviders.tsx index d547bb1d..297a4749 100644 --- a/src/components/Nav/AuthModal/SocialProviders.tsx +++ b/src/components/Nav/AuthModal/SocialProviders/SocialProviders.tsx @@ -1,8 +1,8 @@ import { For } from 'solid-js' -import { useLocalize } from '../../../context/localize' -import { useSession } from '../../../context/session' -import { Icon } from '../../_shared/Icon' +import { useLocalize } from '../../../../context/localize' +import { useSession } from '../../../../context/session' +import { Icon } from '../../../_shared/Icon' import styles from './SocialProviders.module.scss' @@ -18,7 +18,7 @@ export const SocialProviders = () => {
{(provider) => ( - )} diff --git a/src/components/Nav/AuthModal/SocialProviders/index.ts b/src/components/Nav/AuthModal/SocialProviders/index.ts new file mode 100644 index 00000000..07a9863a --- /dev/null +++ b/src/components/Nav/AuthModal/SocialProviders/index.ts @@ -0,0 +1 @@ +export { SocialProviders } from './SocialProviders' diff --git a/src/components/Nav/AuthModal/index.tsx b/src/components/Nav/AuthModal/index.tsx index 760e58a0..d1df5650 100644 --- a/src/components/Nav/AuthModal/index.tsx +++ b/src/components/Nav/AuthModal/index.tsx @@ -16,12 +16,14 @@ import { RegisterForm } from './RegisterForm' import { SendResetLinkForm } from './SendResetLinkForm' import styles from './AuthModal.module.scss' +import { SendEmailConfirm } from './SendEmailConfirm' const AUTH_MODAL_MODES: Record = { login: LoginForm, register: RegisterForm, 'send-reset-link': SendResetLinkForm, 'confirm-email': EmailConfirm, + 'send-confirm-email': SendEmailConfirm, 'change-password': ChangePasswordForm, } diff --git a/src/components/Nav/AuthModal/types.ts b/src/components/Nav/AuthModal/types.ts index e2db3e78..185b5841 100644 --- a/src/components/Nav/AuthModal/types.ts +++ b/src/components/Nav/AuthModal/types.ts @@ -1,4 +1,10 @@ -export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'send-reset-link' | 'change-password' +export type AuthModalMode = + | 'login' + | 'register' + | 'confirm-email' + | 'send-confirm-email' + | 'send-reset-link' + | 'change-password' export type AuthModalSource = | 'discussions' | 'vote' diff --git a/src/components/Topic/TopicBadge/TopicBadge.tsx b/src/components/Topic/TopicBadge/TopicBadge.tsx index f4767986..c2679b1e 100644 --- a/src/components/Topic/TopicBadge/TopicBadge.tsx +++ b/src/components/Topic/TopicBadge/TopicBadge.tsx @@ -115,15 +115,17 @@ export const TopicBadge = (props: Props) => {
- {t('shoutsWithCount', {count: props.topic?.stat?.shouts})} - {t('authorsWithCount', {count: props.topic?.stat?.authors})} + {t('shoutsWithCount', { count: props.topic?.stat?.shouts })} + {t('authorsWithCount', { count: props.topic?.stat?.authors })} - {t('FollowersWithCount', {count: props.topic?.stat?.followers})} + {t('FollowersWithCount', { count: props.topic?.stat?.followers })} - {t('CommentsWithCount', {count: props.topic?.stat?.comments ?? 0})} + + {t('CommentsWithCount', { count: props.topic?.stat?.comments ?? 0 })} +
-
+
) } diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 4e862696..83da483d 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -64,10 +64,11 @@ export const EditView = (props: Props) => { getDraftFromLocalStorage, } = useEditorContext() const shoutTopics = props.shout.topics || [] - const draft = getDraftFromLocalStorage(props.shout.id) + // TODO: проверить сохранение черновика в local storage (не работает) + const draft = getDraftFromLocalStorage(props.shout.id) if (draft) { - setForm(draft) + setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }) } else { setForm({ slug: props.shout.slug, @@ -179,6 +180,7 @@ export const EditView = (props: Props) => { let autoSaveTimeOutId: number | string | NodeJS.Timeout + //TODO: add throttle const autoSaveRecursive = () => { autoSaveTimeOutId = setTimeout(async () => { const hasChanges = !deepEqual(form, prevForm) @@ -307,10 +309,10 @@ export const EditView = (props: Props) => { subtitleInput.current = el }} allowEnterKey={false} - value={(value) => setForm('subtitle', value)} + value={(value) => setForm('subtitle', value || '')} class={styles.subtitleInput} placeholder={t('Subheader')} - initialValue={form.subtitle} + initialValue={form.subtitle || ''} maxLength={MAX_HEADER_LIMIT} /> diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 7bc52d56..47a2dee4 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -70,10 +70,10 @@ export const PublishSettings = (props: Props) => { return { coverImageUrl: props.form?.coverImageUrl, mainTopic: props.form?.mainTopic || EMPTY_TOPIC, - slug: props.form?.slug, - title: props.form?.title, - subtitle: props.form?.subtitle, - description: composeDescription(), + slug: props.form?.slug || '', + title: props.form?.title || '', + subtitle: props.form?.subtitle || '', + description: composeDescription() || '', selectedTopics: [], } }) @@ -100,7 +100,7 @@ export const PublishSettings = (props: Props) => { const handleTopicSelectChange = (newSelectedTopics) => { if ( props.form.selectedTopics.length === 0 || - newSelectedTopics.every((topic) => topic.id !== props.form.mainTopic.id) + newSelectedTopics.every((topic) => topic.id !== props.form.mainTopic?.id) ) { setSettingsForm((prev) => { return { @@ -176,7 +176,7 @@ export const PublishSettings = (props: Props) => {
{settingsForm.mainTopic.title}
{settingsForm.title}
-
{settingsForm.subtitle}
+
{settingsForm.subtitle || ''}
{author()?.name}
@@ -203,7 +203,7 @@ export const PublishSettings = (props: Props) => { variant="bordered" fieldName={t('Subheader')} placeholder={t('Come up with a subtitle for your story')} - initialValue={settingsForm.subtitle} + initialValue={settingsForm.subtitle || ''} value={(value) => setSettingsForm('subtitle', value)} allowEnterKey={false} maxLength={100} diff --git a/src/context/connect.tsx b/src/context/connect.tsx index d10d0695..dfd8d549 100644 --- a/src/context/connect.tsx +++ b/src/context/connect.tsx @@ -50,7 +50,7 @@ export const ConnectProvider = (props: { children: JSX.Element }) => { Authorization: token, }, onmessage(event) { - const m: SSEMessage = JSON.parse(event.data) + const m: SSEMessage = JSON.parse(event.data || '{}') console.log('[context.connect] Received message:', m) // Iterate over all registered handlers and call them diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 5df99775..da9fe026 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -39,7 +39,7 @@ type EditorContextType = { wordCounter: Accessor form: ShoutForm formErrors: Record - editorRef: { current: () => Editor } + editorRef: { current: () => Editor | null } saveShout: (form: ShoutForm) => Promise saveDraft: (form: ShoutForm) => Promise saveDraftToLocalStorage: (form: ShoutForm) => void @@ -72,7 +72,7 @@ const saveDraftToLocalStorage = (formToSave: ShoutForm) => { localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave)) } const getDraftFromLocalStorage = (shoutId: number) => { - return JSON.parse(localStorage.getItem(`shout-${shoutId}`)) + return JSON.parse(localStorage.getItem(`shout-${shoutId}`) || '{}') } const removeDraftFromLocalStorage = (shoutId: number) => { @@ -80,13 +80,19 @@ const removeDraftFromLocalStorage = (shoutId: number) => { } export const EditorProvider = (props: { children: JSX.Element }) => { - const { t } = useLocalize() + const localize = useLocalize() const { page } = useRouter() - const { showSnackbar } = useSnackbar() + const snackbar = useSnackbar() const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal(false) - const editorRef: { current: () => Editor } = { current: null } - const [form, setForm] = createStore(null) - const [formErrors, setFormErrors] = createStore>(null) + const editorRef: { current: () => Editor | null } = { current: () => null } + const [form, setForm] = createStore({ + body: '', + slug: '', + shoutId: 0, + title: '', + selectedTopics: [], + }) + const [formErrors, setFormErrors] = createStore({} as Record) const [wordCounter, setWordCounter] = createSignal({ characters: 0, words: 0, @@ -95,13 +101,16 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const countWords = (value) => setWordCounter(value) const validate = () => { if (!form.title) { - setFormErrors('title', t('Please, set the article title')) + setFormErrors('title', localize?.t('Please, set the article title') || '') return false } - const parsedMedia = JSON.parse(form.media) + const parsedMedia = JSON.parse(form.media || '[]') if (form.layout === 'video' && !parsedMedia[0]) { - showSnackbar({ type: 'error', body: t('Looks like you forgot to upload the video') }) + snackbar?.showSnackbar({ + type: 'error', + body: localize?.t('Looks like you forgot to upload the video'), + }) return false } @@ -110,7 +119,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const validateSettings = () => { if (form.selectedTopics.length === 0) { - setFormErrors('selectedTopics', t('Required')) + setFormErrors('selectedTopics', localize?.t('Required') || '') return false } @@ -118,6 +127,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => { + if (!formToUpdate.shoutId) { + console.error(formToUpdate) + return { error: 'not enought data' } + } return await apiClient.updateArticle({ shout_id: formToUpdate.shoutId, shout_input: { @@ -143,48 +156,61 @@ export const EditorProvider = (props: { children: JSX.Element }) => { toggleEditorPanel() } - if (page().route === 'edit' && !validate()) { + if (page()?.route === 'edit' && !validate()) { return } - if (page().route === 'editSettings' && !validateSettings()) { + if (page()?.route === 'editSettings' && !validateSettings()) { return } try { - const shout = await updateShout(formToSave, { publish: false }) + const { shout, error } = await updateShout(formToSave, { publish: false }) + if (error) { + snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) + return + } removeDraftFromLocalStorage(formToSave.shoutId) - if (shout.published_at) { + if (shout?.published_at) { openPage(router, 'article', { slug: shout.slug }) } else { openPage(router, 'drafts') } } catch (error) { console.error('[saveShout]', error) - showSnackbar({ type: 'error', body: t('Error') }) + snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) } } const saveDraft = async (draftForm: ShoutForm) => { - await updateShout(draftForm, { publish: false }) + const { error } = await updateShout(draftForm, { publish: false }) + if (error) { + snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) + return + } } const publishShout = async (formToPublish: ShoutForm) => { - if (isEditorPanelVisible()) { + const editorPanelVisible = isEditorPanelVisible() + const pageRoute = page()?.route + + if (editorPanelVisible) { toggleEditorPanel() } - if (page().route === 'edit') { + if (pageRoute === 'edit') { if (!validate()) { return } - await updateShout(formToPublish, { publish: false }) - const slug = slugify(form.title) setForm('slug', slug) openPage(router, 'editSettings', { shoutId: form.shoutId.toString() }) + const { error } = await updateShout(formToPublish, { publish: false }) + if (error) { + snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) + } return } @@ -193,20 +219,33 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } try { - await updateShout(formToPublish, { publish: true }) + const { error } = await updateShout(formToPublish, { publish: true }) + if (error) { + snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) + return + } openPage(router, 'feed') } catch (error) { console.error('[publishShout]', error) - showSnackbar({ type: 'error', body: t('Error') }) + snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) } } const publishShoutById = async (shout_id: number) => { + if (!shout_id) { + console.error(`shout_id is ${shout_id}`) + return + } try { - const newShout = await apiClient.updateArticle({ + const { shout: newShout, error } = await apiClient.updateArticle({ shout_id, publish: true, }) + if (error) { + console.error(error) + snackbar?.showSnackbar({ type: 'error', body: error }) + return + } if (newShout) { addArticles([newShout]) openPage(router, 'feed') @@ -215,7 +254,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { } } catch (error) { console.error('[publishShoutById]', error) - showSnackbar({ type: 'error', body: t('Error') }) + snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') }) } } @@ -226,7 +265,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { }) return true } catch { - showSnackbar({ type: 'error', body: t('Error') }) + snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) return false } } diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 8cf91f27..07b36e7d 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -5,7 +5,8 @@ import { createStore, reconcile } from 'solid-js/store' import { apiClient } from '../graphql/client/core' import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen' -import { useSession } from './session' +import { useLocalize } from './localize' +import { useSnackbar } from './snackbar' type ReactionsContextType = { reactionEntities: Accessor> @@ -20,7 +21,7 @@ type ReactionsContextType = { }) => Promise createReaction: (reaction: ReactionInput) => Promise updateReaction: (reaction: ReactionInput) => Promise - deleteReaction: (id: number) => Promise + deleteReaction: (id: number) => Promise<{ error: string }> } const ReactionsContext = createContext() @@ -30,8 +31,9 @@ export function useReactions() { } export const ReactionsProvider = (props: { children: JSX.Element }) => { - const [reactionEntities, setReactionEntities] = createSignal | undefined>() - const { author } = useSession() + const [reactionEntities, setReactionEntities] = createStore>({}) + const { t } = useLocalize() + const { showSnackbar } = useSnackbar() const loadReactionsBy = async ({ by, @@ -55,18 +57,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } const createReaction = async (input: ReactionInput): Promise => { - const fakeId = Date.now() + Math.floor(Math.random() * 1000) - setReactionEntities((rrr: Record) => ({ - ...rrr, - [fakeId]: { - ...input, - id: fakeId, - created_by: author(), - created_at: Math.floor(Date.now() / 1000), - } as unknown as Reaction, - })) - const reaction = await apiClient.createReaction(input) - setReactionEntities({ [fakeId]: undefined }) + const { error, reaction } = await apiClient.createReaction(input) + if (error) await showSnackbar({ type: 'error', body: t(error) }) if (!reaction) return const changes = { [reaction.id]: reaction, @@ -92,19 +84,22 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { setReactionEntities(changes) } - const deleteReaction = async (reaction: number): Promise => { - setReactionEntities({ [reaction]: undefined }) - await apiClient.destroyReaction(reaction) + const deleteReaction = async (reaction_id: number): Promise<{ error: string; reaction?: string }> => { + if (reaction_id) { + const result = await apiClient.destroyReaction(reaction_id) + if (!result.error) { + setReactionEntities({ + [reaction_id]: undefined, + }) + } + return result + } } const updateReaction = async (input: ReactionInput): Promise => { - const reaction = await apiClient.updateReaction(input) - if (reaction) { - setReactionEntities((rrr) => { - rrr[reaction.id] = reaction - return rrr - }) - } + const { error, reaction } = await apiClient.updateReaction(input) + if (error) await showSnackbar({ type: 'error', body: t(error) }) + if (reaction) setReactionEntities(reaction.id, reaction) return reaction } diff --git a/src/graphql/client/core.ts b/src/graphql/client/core.ts index ad3acc04..d7cb4284 100644 --- a/src/graphql/client/core.ts +++ b/src/graphql/client/core.ts @@ -28,6 +28,7 @@ import reactionDestroy from '../mutation/core/reaction-destroy' import reactionUpdate from '../mutation/core/reaction-update' import unfollowMutation from '../mutation/core/unfollow' import shoutLoad from '../query/core/article-load' +import getMyShout from '../query/core/article-my' import shoutsLoadBy from '../query/core/articles-load-by' import draftsLoad from '../query/core/articles-load-drafts' import myFeed from '../query/core/articles-load-feed' @@ -41,7 +42,6 @@ import authorFollows from '../query/core/author-follows' import authorId from '../query/core/author-id' import authorsAll from '../query/core/authors-all' import authorsLoadBy from '../query/core/authors-load-by' -import mySubscriptions from '../query/core/my-followed' import reactionsLoadBy from '../query/core/reactions-load-by' import topicBySlug from '../query/core/topic-by-slug' import topicsAll from '../query/core/topics-all' @@ -135,7 +135,6 @@ export const apiClient = { user?: string }): Promise => { const response = await publicGraphQLClient.query(authorFollows, params).toPromise() - console.log('!!! response:', response) return response.data.get_author_follows }, @@ -162,12 +161,12 @@ export const apiClient = { shout_id: number shout_input?: ShoutInput publish: boolean - }): Promise => { + }): Promise => { const response = await apiClient.private .mutation(updateArticle, { shout_id, shout_input, publish }) .toPromise() console.debug('[graphql.client.core] updateArticle:', response.data) - return response.data.update_shout.shout + return response.data.update_shout }, deleteShout: async (params: MutationDelete_ShoutArgs): Promise => { @@ -178,7 +177,7 @@ export const apiClient = { getDrafts: async (): Promise => { const response = await apiClient.private.query(draftsLoad, {}).toPromise() console.debug('[graphql.client.core] getDrafts:', response) - return response.data.load_shouts_drafts + return response.data.get_shouts_drafts }, createReaction: async (input: ReactionInput) => { const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise() @@ -188,7 +187,7 @@ export const apiClient = { destroyReaction: async (reaction_id: number) => { const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise() console.debug('[graphql.client.core] destroyReaction:', response) - return response.data.delete_reaction.reaction + return response.data.delete_reaction }, updateReaction: async (reaction: ReactionInput) => { const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise() @@ -200,15 +199,18 @@ export const apiClient = { console.debug('[graphql.client.core] authorsLoadBy:', resp) return resp.data.load_authors_by }, + getShoutBySlug: async (slug: string) => { const resp = await publicGraphQLClient.query(shoutLoad, { slug }).toPromise() return resp.data.get_shout }, - getShoutById: async (shout_id: number) => { - const resp = await publicGraphQLClient.query(shoutLoad, { shout_id }).toPromise() + + getMyShout: async (shout_id: number) => { + await apiClient.private + const resp = await apiClient.private.query(getMyShout, { shout_id }).toPromise() if (resp.error) console.error(resp) - return resp.data.get_shout + return resp.data.get_my_shout }, getShouts: async (options: LoadShoutsOptions) => { diff --git a/src/graphql/query/core/article-load.ts b/src/graphql/query/core/article-load.ts index f6965dcb..96605467 100644 --- a/src/graphql/query/core/article-load.ts +++ b/src/graphql/query/core/article-load.ts @@ -1,8 +1,8 @@ import { gql } from '@urql/core' export default gql` - query LoadShoutQuery($slug: String, $shout_id: Int) { - get_shout(slug: $slug, shout_id: $shout_id) { + query LoadShoutQuery($slug: String!) { + get_shout(slug: $slug) { id title lead diff --git a/src/graphql/query/core/article-my.ts b/src/graphql/query/core/article-my.ts new file mode 100644 index 00000000..dcd41ea2 --- /dev/null +++ b/src/graphql/query/core/article-my.ts @@ -0,0 +1,53 @@ +import { gql } from '@urql/core' + +export default gql` + query GetMyShout($shout_id: Int!) { + get_my_shout(shout_id: $shout_id) { + error + shout { + id + title + lead + description + subtitle + slug + layout + cover + cover_caption + body + media + updated_by { + id + name + slug + pic + created_at + } + # community + main_topic + topics { + id + title + body + slug + stat { + shouts + authors + followers + } + } + authors { + id + name + slug + pic + created_at + } + created_at + updated_at + published_at + featured_at + } + } + } +` diff --git a/src/graphql/query/core/articles-load-drafts.ts b/src/graphql/query/core/articles-load-drafts.ts index af33cf9a..f81d0a2f 100644 --- a/src/graphql/query/core/articles-load-drafts.ts +++ b/src/graphql/query/core/articles-load-drafts.ts @@ -2,7 +2,7 @@ import { gql } from '@urql/core' export default gql` query LoadDraftsQuery { - load_shouts_drafts { + get_shouts_drafts { id title subtitle @@ -35,7 +35,6 @@ export default gql` featured_at stat { viewed - rating commented } diff --git a/src/graphql/query/core/topics-all.ts b/src/graphql/query/core/topics-all.ts index ecbbb188..aea60b64 100644 --- a/src/graphql/query/core/topics-all.ts +++ b/src/graphql/query/core/topics-all.ts @@ -13,6 +13,7 @@ export default gql` shouts authors followers + comments # viewed } } diff --git a/src/pages/edit.page.tsx b/src/pages/edit.page.tsx index 031eedf4..ef33f712 100644 --- a/src/pages/edit.page.tsx +++ b/src/pages/edit.page.tsx @@ -7,22 +7,42 @@ import { useLocalize } from '../context/localize' import { apiClient } from '../graphql/client/core' import { Shout } from '../graphql/schema/core.gen' import { useRouter } from '../stores/router' +import { router } from '../stores/router' +import { redirectPage } from '@nanostores/router' +import { useSnackbar } from '../context/snackbar' import { LayoutType } from './types' const EditView = lazy(() => import('../components/Views/EditView/EditView')) export const EditPage = () => { const { page } = useRouter() + const snackbar = useSnackbar() const { t } = useLocalize() - const shoutId = createMemo(() => Number((page().params as Record<'shoutId', string>).shoutId)) - const [shout, setShout] = createSignal(null) + const loadMyShout = async (shout_id: number) => { + if (shout_id) { + const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id) + console.log(loadedShout) + if (error) { + await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') }) + redirectPage(router, 'drafts') + } else { + setShout(loadedShout) + } + } + } onMount(async () => { - const loadedShout = await apiClient.getShoutById(shoutId()) - setShout(loadedShout) + const shout_id = window.location.pathname.split('/').pop() + if (shout_id) { + try { + await loadMyShout(parseInt(shout_id, 10)) + } catch (e) { + console.error(e) + } + } }) const title = createMemo(() => { diff --git a/src/styles/app.scss b/src/styles/app.scss index d563840e..db3a68bc 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -464,7 +464,7 @@ form { } .form-message--error { - color: #d00820; + color: var(--danger-color) !important; } select { diff --git a/src/utils/sortby.ts b/src/utils/sortby.ts index 7566f577..9cd2182d 100644 --- a/src/utils/sortby.ts +++ b/src/utils/sortby.ts @@ -7,7 +7,7 @@ export const byCreated = (a: Shout | Reaction, b: Shout | Reaction) => { } export const byPublished = (a: Shout, b: Shout) => { - return a.published_at - b.published_at + return (a?.published_at || 0) - (b?.published_at || 0) } export const byLength = (