Merge branch 'dev' into feature/rating
This commit is contained in:
commit
cc951c305b
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -22,4 +22,5 @@ bun.lockb
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/plawright-report/
|
/plawright-report/
|
||||||
|
target
|
||||||
.venv
|
.venv
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"Add signature": "Add signature",
|
"Add signature": "Add signature",
|
||||||
"Add subtitle": "Add subtitle",
|
"Add subtitle": "Add subtitle",
|
||||||
"Add url": "Add url",
|
"Add url": "Add url",
|
||||||
|
"try": "попробуйте",
|
||||||
"Add": "Add",
|
"Add": "Add",
|
||||||
"Address on Discours": "Address on Discours",
|
"Address on Discours": "Address on Discours",
|
||||||
"Album name": "Название aльбома",
|
"Album name": "Название aльбома",
|
||||||
|
@ -144,7 +145,6 @@
|
||||||
"Enter your new password": "Enter your new password",
|
"Enter your new password": "Enter your new password",
|
||||||
"Enter": "Enter",
|
"Enter": "Enter",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"Please give us your email address": "Please provide us your email address to get the password reset link",
|
|
||||||
"Experience": "Experience",
|
"Experience": "Experience",
|
||||||
"FAQ": "Tips and suggestions",
|
"FAQ": "Tips and suggestions",
|
||||||
"Favorite topics": "Favorite topics",
|
"Favorite topics": "Favorite topics",
|
||||||
|
@ -254,7 +254,6 @@
|
||||||
"Nothing here yet": "There's nothing here yet",
|
"Nothing here yet": "There's nothing here yet",
|
||||||
"Nothing is here": "There is nothing here",
|
"Nothing is here": "There is nothing here",
|
||||||
"Notifications": "Notifications",
|
"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",
|
"Or paste a link to an image": "Or paste a link to an image",
|
||||||
"Ordered list": "Ordered list",
|
"Ordered list": "Ordered list",
|
||||||
"Our regular contributor": "Our regular contributor",
|
"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!",
|
"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 link again": "Send link again",
|
||||||
"Send": "Send",
|
"Send": "Send",
|
||||||
"Set the new password": "Set the new password",
|
"Forgot password?": "Forgot password?",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Share publication": "Share publication",
|
"Share publication": "Share publication",
|
||||||
"Share": "Share",
|
"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 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?",
|
"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 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": "This email is",
|
||||||
"This email is not verified": "This email is not verified",
|
"This email is not verified": "This email is not verified",
|
||||||
"This email is verified": "This email is verified",
|
"This email is verified": "This email is verified",
|
||||||
|
@ -525,5 +525,8 @@
|
||||||
"video": "video",
|
"video": "video",
|
||||||
"view": "view",
|
"view": "view",
|
||||||
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,8 +149,8 @@
|
||||||
"Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
|
"Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
|
||||||
"Enter your new password": "Введите новый пароль",
|
"Enter your new password": "Введите новый пароль",
|
||||||
"Enter": "Войти",
|
"Enter": "Войти",
|
||||||
|
"This content is not published yet": "Содержимое ещё не опубликовано",
|
||||||
"Error": "Ошибка",
|
"Error": "Ошибка",
|
||||||
"Please give us your email address": "Пожалуйста, укажите свою почту, чтобы получить ссылку для сброса пароля",
|
|
||||||
"Experience": "Личный опыт",
|
"Experience": "Личный опыт",
|
||||||
"FAQ": "Советы и предложения",
|
"FAQ": "Советы и предложения",
|
||||||
"Favorite topics": "Избранные темы",
|
"Favorite topics": "Избранные темы",
|
||||||
|
@ -266,7 +266,6 @@
|
||||||
"Nothing here yet": "Здесь пока ничего нет",
|
"Nothing here yet": "Здесь пока ничего нет",
|
||||||
"Nothing is here": "Здесь ничего нет",
|
"Nothing is 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": "Теперь можете ввести новый пароль, он должен содержать минимум 8 символов и не совпадать с предыдущим паролем",
|
|
||||||
"Or paste a link to an image": "Или вставьте ссылку на изображение",
|
"Or paste a link to an image": "Или вставьте ссылку на изображение",
|
||||||
"Ordered list": "Нумерованный список",
|
"Ordered list": "Нумерованный список",
|
||||||
"Our regular contributor": "Наш постоянный автор",
|
"Our regular contributor": "Наш постоянный автор",
|
||||||
|
@ -287,7 +286,7 @@
|
||||||
"Pin": "Закрепить",
|
"Pin": "Закрепить",
|
||||||
"Platform Guide": "Гид по дискурсу",
|
"Platform Guide": "Гид по дискурсу",
|
||||||
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
|
"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 confirm your email to finish": "Подтвердите почту и действие совершится",
|
||||||
"Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте",
|
"Please enter a name to sign your comments and publication": "Пожалуйста, введите имя, которое будет отображаться на сайте",
|
||||||
"Please enter email": "Пожалуйста, введите почту",
|
"Please enter email": "Пожалуйста, введите почту",
|
||||||
|
@ -328,7 +327,7 @@
|
||||||
"Reports": "Репортажи",
|
"Reports": "Репортажи",
|
||||||
"Required": "Поле обязательно для заполнения",
|
"Required": "Поле обязательно для заполнения",
|
||||||
"Resend code": "Выслать подтверждение",
|
"Resend code": "Выслать подтверждение",
|
||||||
"Set the new password": "Задать новый пароль",
|
"Forgot password?": "Забыли пароль?",
|
||||||
"Rules of the journal Discours": "Правила журнала Дискурс",
|
"Rules of the journal Discours": "Правила журнала Дискурс",
|
||||||
"Save draft": "Сохранить черновик",
|
"Save draft": "Сохранить черновик",
|
||||||
"Save settings": "Сохранить настройки",
|
"Save settings": "Сохранить настройки",
|
||||||
|
@ -404,6 +403,7 @@
|
||||||
"This email is": "Этот email",
|
"This email is": "Этот email",
|
||||||
"This email is not verified": "Этот email не подтвержден",
|
"This email is not verified": "Этот email не подтвержден",
|
||||||
"This email is verified": "Этот email подтвержден",
|
"This email is verified": "Этот email подтвержден",
|
||||||
|
"try": "попробуйте",
|
||||||
"This email is registered": "Этот email уже зарегистрирован",
|
"This email is registered": "Этот email уже зарегистрирован",
|
||||||
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
|
"This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.",
|
||||||
"This month": "За месяц",
|
"This month": "За месяц",
|
||||||
|
@ -531,6 +531,7 @@
|
||||||
"repeat": "повторить",
|
"repeat": "повторить",
|
||||||
"resend confirmation link": "отправить ссылку ещё раз",
|
"resend confirmation link": "отправить ссылку ещё раз",
|
||||||
"shout": "пост",
|
"shout": "пост",
|
||||||
|
"shout not found": "публикация не найдена",
|
||||||
"shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}",
|
"shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}",
|
||||||
"sign in": "войти",
|
"sign in": "войти",
|
||||||
"sign up or sign in": "зарегистрироваться или войти",
|
"sign up or sign in": "зарегистрироваться или войти",
|
||||||
|
@ -551,5 +552,8 @@
|
||||||
"video": "видео",
|
"video": "видео",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
|
"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": "Восстановить пароль"
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,14 +64,19 @@ export const Comment = (props: Props) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
await deleteReaction(props.comment.id)
|
const { error } = await deleteReaction(props.comment.id)
|
||||||
// TODO: Учесть то что deleteReaction может вернуть error
|
const notificationType = error ? 'error' : 'success'
|
||||||
if (props.onDelete) {
|
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)
|
props.onDelete(props.comment.id)
|
||||||
}
|
}
|
||||||
await showSnackbar({ body: t('Comment successfully deleted') })
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await showSnackbar({ body: 'error' })
|
||||||
console.error('[deleteReaction]', error)
|
console.error('[deleteReaction]', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
@include font-size(1.2rem);
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
color: var(--secondary-color);
|
color: var(--secondary-color);
|
||||||
|
|
||||||
// align-self: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
|
@ -141,7 +141,7 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
const media = createMemo<MediaItem[]>(() => {
|
const media = createMemo<MediaItem[]>(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(props.article.media)
|
return JSON.parse(props.article?.media || '[]')
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,19 +67,19 @@ const getTitleAndSubtitle = (
|
||||||
subtitle: string
|
subtitle: string
|
||||||
} => {
|
} => {
|
||||||
let title = article.title
|
let title = article.title
|
||||||
let subtitle = article.subtitle
|
let subtitle: string = article.subtitle || ''
|
||||||
|
|
||||||
if (!subtitle) {
|
if (!subtitle) {
|
||||||
let tt = article.title?.split('. ') || []
|
let titleParts = article.title?.split('. ') || []
|
||||||
|
|
||||||
if (tt?.length === 1) {
|
if (titleParts?.length === 1) {
|
||||||
tt = article.title?.split(/{!|\?|:|;}\s/) || []
|
titleParts = article.title?.split(/{!|\?|:|;}\s/) || []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tt && tt.length > 1) {
|
if (titleParts && titleParts.length > 1) {
|
||||||
const sep = article.title?.replace(tt[0], '').split(' ', 1)[0]
|
const sep = article.title?.replace(titleParts[0], '').split(' ', 1)[0]
|
||||||
title = tt[0] + (sep === '.' || sep === ':' ? '' : sep)
|
title = titleParts[0] + (sep === '.' || sep === ':' ? '' : sep)
|
||||||
subtitle = capitalize(article.title?.replace(tt[0] + sep, ''), true)
|
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 { title, subtitle } = getTitleAndSubtitle(props.article)
|
||||||
|
|
||||||
const formattedDate = createMemo<string>(() =>
|
const formattedDate = createMemo<string>(() =>
|
||||||
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(
|
const canEdit = createMemo(
|
||||||
|
@ -135,6 +135,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
scrollTo: 'comments',
|
scrollTo: 'comments',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
|
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
|
||||||
|
@ -153,7 +154,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
[aspectRatio()]: props.withAspectRatio,
|
[aspectRatio()]: props.withAspectRatio,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{/* Cover Image */}
|
||||||
<Show when={!(props.settings?.noimage || props.settings?.isFeedMode)}>
|
<Show when={!(props.settings?.noimage || props.settings?.isFeedMode)}>
|
||||||
|
{/* Cover Image Container */}
|
||||||
<div class={styles.shoutCardCoverContainer}>
|
<div class={styles.shoutCardCoverContainer}>
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.shoutCardCover, {
|
class={clsx(styles.shoutCardCover, {
|
||||||
|
@ -178,7 +181,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Shout Card Content */}
|
||||||
<div class={styles.shoutCardContent}>
|
<div class={styles.shoutCardContent}>
|
||||||
|
{/* Shout Card Icon */}
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
props.article.layout &&
|
props.article.layout &&
|
||||||
|
@ -195,6 +201,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Main Topic */}
|
||||||
<Show when={!props.settings?.isGroup && mainTopicSlug}>
|
<Show when={!props.settings?.isGroup && mainTopicSlug}>
|
||||||
<CardTopic
|
<CardTopic
|
||||||
title={mainTopicTitle}
|
title={mainTopicTitle}
|
||||||
|
@ -205,6 +212,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Title and Subtitle */}
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.shoutCardTitlesContainer, {
|
class={clsx(styles.shoutCardTitlesContainer, {
|
||||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
|
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
|
||||||
|
@ -224,22 +232,23 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Details */}
|
||||||
<Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
|
<Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
|
||||||
|
{/* Author and Date */}
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
|
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
|
||||||
>
|
>
|
||||||
<Show when={!props.settings?.noauthor}>
|
<Show when={!props.settings?.noauthor}>
|
||||||
<div class={styles.shoutAuthor}>
|
<div class={styles.shoutAuthor}>
|
||||||
<For each={props.article.authors}>
|
<For each={props.article.authors}>
|
||||||
{(a: Author) => {
|
{(a: Author) => (
|
||||||
return (
|
<AuthorLink
|
||||||
<AuthorLink
|
size={'XS'}
|
||||||
size={'XS'}
|
author={a}
|
||||||
author={a}
|
isFloorImportant={props.settings.isFloorImportant || props.settings?.isWithCover}
|
||||||
isFloorImportant={props.settings.isFloorImportant || props.settings?.isWithCover}
|
/>
|
||||||
/>
|
)}
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -248,6 +257,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
<Show when={props.article.description}>
|
<Show when={props.article.description}>
|
||||||
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -51,7 +51,15 @@ const DialogAvatar = (props: Props) => {
|
||||||
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
|
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
|
||||||
<div
|
<div
|
||||||
class={styles.imageHolder}
|
class={styles.imageHolder}
|
||||||
style={{ 'background-image': `url(${getImageUrl(props.url, { width: 40, height: 40 })})` }}
|
style={{
|
||||||
|
'background-image': `url(
|
||||||
|
${
|
||||||
|
props.url.includes('discours.io')
|
||||||
|
? getImageUrl(props.url, { width: 40, height: 40 })
|
||||||
|
: props.url
|
||||||
|
}
|
||||||
|
)`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.view {
|
.view {
|
||||||
background: #fff;
|
background: var(--background-color);
|
||||||
min-height: 550px;
|
min-height: 550px;
|
||||||
position: relative;
|
position: relative;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -154,17 +154,6 @@
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authInfo {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: smaller;
|
|
||||||
margin-top: -2em;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
.warn {
|
|
||||||
color: #a00;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.authForm {
|
.authForm {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -221,3 +210,7 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
margin-bottom: 52px;
|
margin-bottom: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.submitError {
|
||||||
|
margin: -1rem 0 -2rem;
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const ChangePasswordForm = () => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
if (newPassword()) {
|
if (newPassword()) {
|
||||||
await changePassword(newPassword(), searchParams()?.token)
|
changePassword(newPassword(), searchParams()?.token)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
setIsSuccess(true)
|
setIsSuccess(true)
|
||||||
|
@ -60,11 +60,6 @@ export const ChangePasswordForm = () => {
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h4>{t('Enter a new password')}</h4>
|
<h4>{t('Enter a new password')}</h4>
|
||||||
<div class={styles.authSubtitle}>
|
|
||||||
{t(
|
|
||||||
'Now you can enter a new password, it must contain at least 8 characters and not be the same as the previous password',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Show when={validationErrors()}>
|
<Show when={validationErrors()}>
|
||||||
<div>{validationErrors().password}</div>
|
<div>{validationErrors().password}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -17,19 +17,20 @@ export const EmailConfirm = () => {
|
||||||
const [emailConfirmed, setEmailConfirmed] = createSignal(false)
|
const [emailConfirmed, setEmailConfirmed] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const e = session()?.user?.email
|
const email = session()?.user?.email
|
||||||
const v = session()?.user?.email_verified
|
const isVerified = session()?.user?.email_verified
|
||||||
if (e) {
|
|
||||||
setEmail(e.toLowerCase())
|
if (email) {
|
||||||
if (v) setEmailConfirmed(v)
|
setEmail(email.toLowerCase())
|
||||||
|
if (isVerified) setEmailConfirmed(isVerified)
|
||||||
if (authError()) {
|
if (authError()) {
|
||||||
changeSearchParams({}, true)
|
changeSearchParams({}, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
if (authError()) {
|
||||||
if (authError()) console.debug('[AuthModal.EmailConfirm] auth error:', authError())
|
console.debug('[AuthModal.EmailConfirm] auth error:', authError())
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createSignal } from 'solid-js'
|
import { JSX, Show, createEffect, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -27,12 +27,11 @@ type ValidationErrors = Partial<Record<keyof FormFields, string>>
|
||||||
export const LoginForm = () => {
|
export const LoginForm = () => {
|
||||||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
const [submitError, setSubmitError] = createSignal<string | JSX.Element>()
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
// TODO: better solution for interactive error messages
|
|
||||||
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
|
@ -52,43 +51,43 @@ export const LoginForm = () => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
setIsLinkSent(true)
|
setIsLinkSent(true)
|
||||||
setIsEmailNotConfirmed(false)
|
setSubmitError()
|
||||||
setSubmitError('')
|
changeSearchParams({ mode: 'send-confirm-email' })
|
||||||
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')
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const preSendValidate = async (value: string, type: 'email' | 'password'): Promise<boolean> => {
|
||||||
|
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) => {
|
const handleSubmit = async (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
await preSendValidate(email(), 'email')
|
||||||
|
await preSendValidate(password(), 'password')
|
||||||
|
|
||||||
setIsLinkSent(false)
|
setIsLinkSent(false)
|
||||||
setIsEmailNotConfirmed(false)
|
setSubmitError()
|
||||||
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)
|
|
||||||
|
|
||||||
|
if (Object.keys(validationErrors()).length > 0) {
|
||||||
authFormRef.current
|
authFormRef.current
|
||||||
.querySelector<HTMLInputElement>(`input[name="${Object.keys(newValidationErrors)[0]}"]`)
|
.querySelector<HTMLInputElement>(`input[name="${Object.keys(validationErrors())[0]}"]`)
|
||||||
?.focus()
|
?.focus()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,14 +95,27 @@ export const LoginForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { errors } = await signIn({ email: email(), password: password() })
|
const { errors } = await signIn({ email: email(), password: password() })
|
||||||
|
console.error('[signIn errors]', errors)
|
||||||
if (errors?.length > 0) {
|
if (errors?.length > 0) {
|
||||||
if (errors.some((error) => error.message.includes('bad user credentials'))) {
|
if (errors.some((error) => error.message.includes('bad user credentials'))) {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
password: t('Something went wrong, check email and password'),
|
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(
|
||||||
|
<div class={styles.info}>
|
||||||
|
{t('This email is not verified')}
|
||||||
|
{'. '}
|
||||||
|
<span class={'link'} onClick={handleSendLinkAgainClick}>
|
||||||
|
{t('Send link again')}
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setSubmitError(t('Error'))
|
setSubmitError(t('Error', errors[0].message))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -121,19 +133,6 @@ export const LoginForm = () => {
|
||||||
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
|
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
|
||||||
<div>
|
<div>
|
||||||
<AuthModalHeader modalType="login" />
|
<AuthModalHeader modalType="login" />
|
||||||
<Show when={submitError()}>
|
|
||||||
<div class={styles.authInfo}>
|
|
||||||
<div class={styles.warn}>{submitError()}</div>
|
|
||||||
<Show when={isEmailNotConfirmed()}>
|
|
||||||
<span class={'link'} onClick={handleSendLinkAgainClick}>
|
|
||||||
{t('Send link again')}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={isLinkSent()}>
|
|
||||||
<div class={styles.authInfo}>{t('Link sent, check your email')}</div>
|
|
||||||
</Show>
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().email,
|
'pretty-form__item--error': validationErrors().email,
|
||||||
|
@ -154,11 +153,14 @@ export const LoginForm = () => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasswordField variant={'login'} onInput={(value) => handlePasswordInput(value)} />
|
<PasswordField
|
||||||
<Show when={validationErrors().password}>
|
variant={'login'}
|
||||||
<div class={styles.validationError} style={{ position: 'static', 'font-size': '1.4rem' }}>
|
setError={validationErrors().password}
|
||||||
{validationErrors().password}
|
onInput={(value) => handlePasswordInput(value)}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
|
<Show when={submitError()}>
|
||||||
|
<div class={clsx('form-message--error', styles.submitError)}>{submitError()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -175,7 +177,7 @@ export const LoginForm = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('Set the new password')}
|
{t('Forgot password?')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,11 +31,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Red/500 */
|
/* Red/500 */
|
||||||
color: #d00820;
|
color: orange;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #d00820;
|
color: orange;
|
||||||
border-color: #d00820;
|
border-color: orange;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--default-color-invert);
|
color: var(--default-color-invert);
|
||||||
|
|
|
@ -11,21 +11,23 @@ type Props = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
errorMessage?: (error: string) => void
|
errorMessage?: (error: string) => void
|
||||||
|
setError?: string
|
||||||
onInput: (value: string) => void
|
onInput: (value: string) => void
|
||||||
|
onBlur?: (value: string) => void
|
||||||
variant?: 'login' | 'registration'
|
variant?: 'login' | 'registration'
|
||||||
disableAutocomplete?: boolean
|
disableAutocomplete?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minLength = 8
|
||||||
|
const hasNumber = /\d/
|
||||||
|
const hasSpecial = /[!#$%&*@^]/
|
||||||
|
|
||||||
export const PasswordField = (props: Props) => {
|
export const PasswordField = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [showPassword, setShowPassword] = createSignal(false)
|
const [showPassword, setShowPassword] = createSignal(false)
|
||||||
const [error, setError] = createSignal<string>()
|
const [error, setError] = createSignal<string>()
|
||||||
|
|
||||||
const validatePassword = (passwordToCheck) => {
|
const validatePassword = (passwordToCheck) => {
|
||||||
const minLength = 8
|
|
||||||
const hasNumber = /\d/
|
|
||||||
const hasSpecial = /[!#$%&*@^]/
|
|
||||||
|
|
||||||
if (passwordToCheck.length < minLength) {
|
if (passwordToCheck.length < minLength) {
|
||||||
return t('Password should be at least 8 characters')
|
return t('Password should be at least 8 characters')
|
||||||
}
|
}
|
||||||
|
@ -35,11 +37,17 @@ export const PasswordField = (props: Props) => {
|
||||||
if (!hasSpecial.test(passwordToCheck)) {
|
if (!hasSpecial.test(passwordToCheck)) {
|
||||||
return t('Password should contain at least one special character: !@#$%^&*')
|
return t('Password should contain at least one special character: !@#$%^&*')
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
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)
|
props.onInput(value)
|
||||||
const errorValue = validatePassword(value)
|
const errorValue = validatePassword(value)
|
||||||
if (errorValue) {
|
if (errorValue) {
|
||||||
|
@ -58,14 +66,13 @@ export const PasswordField = (props: Props) => {
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
createEffect(() => {
|
||||||
|
setError(props.setError)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.PassportField, props.class)}>
|
<div class={clsx(styles.PassportField, props.class)}>
|
||||||
<div
|
<div class="pretty-form__item">
|
||||||
class={clsx('pretty-form__item', {
|
|
||||||
'pretty-form__item--error': error() && props.variant !== 'login',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
@ -73,7 +80,7 @@ export const PasswordField = (props: Props) => {
|
||||||
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
|
autocomplete={props.disableAutocomplete ? 'one-time-code' : 'current-password'}
|
||||||
type={showPassword() ? 'text' : 'password'}
|
type={showPassword() ? 'text' : 'password'}
|
||||||
placeholder={props.placeholder || t('Password')}
|
placeholder={props.placeholder || t('Password')}
|
||||||
onInput={(event) => handleInputChange(event.currentTarget.value)}
|
onBlur={(event) => handleInputBlur(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="password">{t('Password')}</label>
|
<label for="password">{t('Password')}</label>
|
||||||
<button
|
<button
|
||||||
|
@ -83,8 +90,14 @@ export const PasswordField = (props: Props) => {
|
||||||
>
|
>
|
||||||
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
<Icon class={styles.passwordToggleIcon} name={showPassword() ? 'eye-off' : 'eye'} />
|
||||||
</button>
|
</button>
|
||||||
<Show when={error() && props.variant !== 'login'}>
|
<Show when={error()}>
|
||||||
<div class={clsx(styles.registerPassword, styles.validationError)}>{error()}</div>
|
<div
|
||||||
|
class={clsx(styles.registerPassword, styles.validationError, {
|
||||||
|
'form-message--error': props.setError,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,10 +28,6 @@ type FormFields = {
|
||||||
|
|
||||||
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
const handleEmailInput = (newEmail: string) => {
|
|
||||||
setEmail(newEmail.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RegisterForm = () => {
|
export const RegisterForm = () => {
|
||||||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -137,7 +133,8 @@ export const RegisterForm = () => {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
email: (
|
email: (
|
||||||
<>
|
<>
|
||||||
{t('This email is verified')}. {t('You can')}{' '}
|
{t('This email is registered')}. {t('try')}
|
||||||
|
{', '}
|
||||||
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
|
<span class="link" onClick={() => changeSearchParams({ mode: 'login' })}>
|
||||||
{t('enter')}
|
{t('enter')}
|
||||||
</span>
|
</span>
|
||||||
|
@ -150,9 +147,10 @@ export const RegisterForm = () => {
|
||||||
...prev,
|
...prev,
|
||||||
email: (
|
email: (
|
||||||
<>
|
<>
|
||||||
{t('This email is registered')}. {t('You can')}{' '}
|
{t('This email is registered')}
|
||||||
|
{'. '}
|
||||||
<span class="link" onClick={() => changeSearchParams({ mode: 'send-reset-link' })}>
|
<span class="link" onClick={() => changeSearchParams({ mode: 'send-reset-link' })}>
|
||||||
{t('Set the new password').toLocaleLowerCase()}
|
{t('Set the new password')}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
@ -172,17 +170,18 @@ export const RegisterForm = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setEmailStatus('')
|
||||||
|
setValidationErrors({})
|
||||||
|
setEmail(newEmail.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Show when={!isSuccess()}>
|
<Show when={!isSuccess()}>
|
||||||
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
|
<form onSubmit={handleSubmit} class={styles.authForm} ref={(el) => (authFormRef.current = el)}>
|
||||||
<div>
|
<div>
|
||||||
<AuthModalHeader modalType="register" />
|
<AuthModalHeader modalType="register" />
|
||||||
<Show when={submitError()}>
|
|
||||||
<div class={styles.authInfo}>
|
|
||||||
<div class={styles.warn}>{submitError()}</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().fullName,
|
'pretty-form__item--error': validationErrors().fullName,
|
||||||
|
@ -194,7 +193,7 @@ export const RegisterForm = () => {
|
||||||
disabled={Boolean(emailStatus())}
|
disabled={Boolean(emailStatus())}
|
||||||
placeholder={t('Full name')}
|
placeholder={t('Full name')}
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
onChange={(event) => handleNameInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="name">{t('Full name')}</label>
|
<label for="name">{t('Full name')}</label>
|
||||||
<Show when={validationErrors().fullName && !emailStatus()}>
|
<Show when={validationErrors().fullName && !emailStatus()}>
|
||||||
|
@ -217,16 +216,18 @@ export const RegisterForm = () => {
|
||||||
onBlur={handleEmailBlur}
|
onBlur={handleEmailBlur}
|
||||||
/>
|
/>
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
<div class={clsx(styles.validationError, { info: Boolean(emailStatus()) })}>
|
<Show when={validationErrors().email || emailStatus()}>
|
||||||
{validationErrors().email}
|
<div class={clsx(styles.validationError, { info: Boolean(emailStatus()) })}>
|
||||||
</div>
|
{validationErrors().email}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PasswordField
|
<PasswordField
|
||||||
disableAutocomplete={true}
|
disableAutocomplete={true}
|
||||||
disabled={Boolean(emailStatus())}
|
disabled={Boolean(emailStatus())}
|
||||||
errorMessage={(err) => setPasswordError(err)}
|
errorMessage={(err) => !emailStatus() && setPasswordError(err)}
|
||||||
onInput={(value) => setPassword(value)}
|
onInput={(value) => setPassword(emailStatus() ? '' : value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -259,12 +260,14 @@ export const RegisterForm = () => {
|
||||||
</form>
|
</form>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={isSuccess()}>
|
<Show when={isSuccess()}>
|
||||||
<div class={styles.title}>{t('Almost done! Check your email.')}</div>
|
<div style={{ 'justify-content': 'center' }}>
|
||||||
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
|
<div class={styles.title}>{t('Almost done! Check your email.')}</div>
|
||||||
<div>
|
<div class={styles.text}>{t("We've sent you a message with a link to enter our website.")}</div>
|
||||||
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
<div>
|
||||||
{t('Back to main page')}
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
</button>
|
{t('Back to main page')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
|
|
24
src/components/Nav/AuthModal/SendEmailConfirm.tsx
Normal file
24
src/components/Nav/AuthModal/SendEmailConfirm.tsx
Normal file
|
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
'align-items': 'center',
|
||||||
|
'justify-content': 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class={styles.text}>{t('Link sent, check your email')}</div>
|
||||||
|
<div>
|
||||||
|
<button class={clsx('button', styles.submitButton)} onClick={() => hideModal()}>
|
||||||
|
{t('Go to main page')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
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 { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -72,6 +72,12 @@ export const SendResetLinkForm = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (email()) {
|
||||||
|
console.info('[SendResetLinkForm] email detected')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
@ -79,8 +85,12 @@ export const SendResetLinkForm = () => {
|
||||||
ref={(el) => (authFormRef.current = el)}
|
ref={(el) => (authFormRef.current = el)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h4>{t('Set the new password')}</h4>
|
<h4>{t('Forgot password?')}</h4>
|
||||||
<div class={styles.authSubtitle}>{t(message()) || t('Please give us your email address')}</div>
|
<Show when={!message()}>
|
||||||
|
<div class={styles.authSubtitle}>
|
||||||
|
{t("It's OK. Just enter your email to receive a link to change your password")}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
<div
|
<div
|
||||||
class={clsx('pretty-form__item', {
|
class={clsx('pretty-form__item', {
|
||||||
'pretty-form__item--error': validationErrors().email,
|
'pretty-form__item--error': validationErrors().email,
|
||||||
|
@ -94,7 +104,7 @@ export const SendResetLinkForm = () => {
|
||||||
type="email"
|
type="email"
|
||||||
value={email()}
|
value={email()}
|
||||||
placeholder={t('Email')}
|
placeholder={t('Email')}
|
||||||
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
onChange={(event) => handleEmailInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
<Show when={isUserNotFound()}>
|
<Show when={isUserNotFound()}>
|
||||||
|
@ -104,7 +114,7 @@ export const SendResetLinkForm = () => {
|
||||||
class={'link'}
|
class={'link'}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
changeSearchParams({
|
changeSearchParams({
|
||||||
mode: 'login',
|
mode: 'register',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -116,28 +126,31 @@ export const SendResetLinkForm = () => {
|
||||||
<div class={styles.validationError}>{validationErrors().email}</div>
|
<div class={styles.validationError}>{validationErrors().email}</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={!message()} fallback={<div class={styles.authSubtitle}>{t(message())}</div>}>
|
||||||
<div style={{ 'margin-top': '5rem' }}>
|
<>
|
||||||
<button
|
<div style={{ 'margin-top': '5rem' }}>
|
||||||
class={clsx('button', styles.submitButton)}
|
<button
|
||||||
disabled={isSubmitting() || Boolean(message())}
|
class={clsx('button', styles.submitButton)}
|
||||||
type="submit"
|
disabled={isSubmitting() || Boolean(message())}
|
||||||
>
|
type="submit"
|
||||||
{isSubmitting() ? '...' : t('Send')}
|
>
|
||||||
</button>
|
{isSubmitting() ? '...' : t('Restore password')}
|
||||||
</div>
|
</button>
|
||||||
<div class={styles.authControl}>
|
</div>
|
||||||
<span
|
<div class={styles.authControl}>
|
||||||
class={styles.authLink}
|
<span
|
||||||
onClick={() =>
|
class={styles.authLink}
|
||||||
changeSearchParams({
|
onClick={() =>
|
||||||
mode: 'login',
|
changeSearchParams({
|
||||||
})
|
mode: 'login',
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
{t('I know the password')}
|
>
|
||||||
</span>
|
{t('I know the password')}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../../context/session'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../../_shared/Icon'
|
||||||
|
|
||||||
import styles from './SocialProviders.module.scss'
|
import styles from './SocialProviders.module.scss'
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export const SocialProviders = () => {
|
||||||
<div class={styles.social}>
|
<div class={styles.social}>
|
||||||
<For each={PROVIDERS}>
|
<For each={PROVIDERS}>
|
||||||
{(provider) => (
|
{(provider) => (
|
||||||
<button class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
<button type="button" class={styles[provider]} onClick={(_e) => oauth(provider)}>
|
||||||
<Icon name={provider} />
|
<Icon name={provider} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
1
src/components/Nav/AuthModal/SocialProviders/index.ts
Normal file
1
src/components/Nav/AuthModal/SocialProviders/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { SocialProviders } from './SocialProviders'
|
|
@ -16,12 +16,14 @@ import { RegisterForm } from './RegisterForm'
|
||||||
import { SendResetLinkForm } from './SendResetLinkForm'
|
import { SendResetLinkForm } from './SendResetLinkForm'
|
||||||
|
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
|
import { SendEmailConfirm } from './SendEmailConfirm'
|
||||||
|
|
||||||
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
|
||||||
login: LoginForm,
|
login: LoginForm,
|
||||||
register: RegisterForm,
|
register: RegisterForm,
|
||||||
'send-reset-link': SendResetLinkForm,
|
'send-reset-link': SendResetLinkForm,
|
||||||
'confirm-email': EmailConfirm,
|
'confirm-email': EmailConfirm,
|
||||||
|
'send-confirm-email': SendEmailConfirm,
|
||||||
'change-password': ChangePasswordForm,
|
'change-password': ChangePasswordForm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 =
|
export type AuthModalSource =
|
||||||
| 'discussions'
|
| 'discussions'
|
||||||
| 'vote'
|
| 'vote'
|
||||||
|
|
|
@ -115,15 +115,17 @@ export const TopicBadge = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.stats}>
|
<div class={styles.stats}>
|
||||||
<span class={styles.statsItem}>{t('shoutsWithCount', {count: props.topic?.stat?.shouts})}</span>
|
<span class={styles.statsItem}>{t('shoutsWithCount', { count: props.topic?.stat?.shouts })}</span>
|
||||||
<span class={styles.statsItem}>{t('authorsWithCount', {count: props.topic?.stat?.authors})}</span>
|
<span class={styles.statsItem}>{t('authorsWithCount', { count: props.topic?.stat?.authors })}</span>
|
||||||
<span class={styles.statsItem}>
|
<span class={styles.statsItem}>
|
||||||
{t('FollowersWithCount', {count: props.topic?.stat?.followers})}
|
{t('FollowersWithCount', { count: props.topic?.stat?.followers })}
|
||||||
</span>
|
</span>
|
||||||
<Show when={props.topic?.stat?.comments}>
|
<Show when={props.topic?.stat?.comments}>
|
||||||
<span class={styles.statsItem}>{t('CommentsWithCount', {count: props.topic?.stat?.comments ?? 0})}</span>
|
<span class={styles.statsItem}>
|
||||||
|
{t('CommentsWithCount', { count: props.topic?.stat?.comments ?? 0 })}
|
||||||
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,10 +64,11 @@ export const EditView = (props: Props) => {
|
||||||
getDraftFromLocalStorage,
|
getDraftFromLocalStorage,
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
const shoutTopics = props.shout.topics || []
|
const shoutTopics = props.shout.topics || []
|
||||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
|
||||||
|
|
||||||
|
// TODO: проверить сохранение черновика в local storage (не работает)
|
||||||
|
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||||
if (draft) {
|
if (draft) {
|
||||||
setForm(draft)
|
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id })
|
||||||
} else {
|
} else {
|
||||||
setForm({
|
setForm({
|
||||||
slug: props.shout.slug,
|
slug: props.shout.slug,
|
||||||
|
@ -179,6 +180,7 @@ export const EditView = (props: Props) => {
|
||||||
|
|
||||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||||
|
|
||||||
|
//TODO: add throttle
|
||||||
const autoSaveRecursive = () => {
|
const autoSaveRecursive = () => {
|
||||||
autoSaveTimeOutId = setTimeout(async () => {
|
autoSaveTimeOutId = setTimeout(async () => {
|
||||||
const hasChanges = !deepEqual(form, prevForm)
|
const hasChanges = !deepEqual(form, prevForm)
|
||||||
|
@ -307,10 +309,10 @@ export const EditView = (props: Props) => {
|
||||||
subtitleInput.current = el
|
subtitleInput.current = el
|
||||||
}}
|
}}
|
||||||
allowEnterKey={false}
|
allowEnterKey={false}
|
||||||
value={(value) => setForm('subtitle', value)}
|
value={(value) => setForm('subtitle', value || '')}
|
||||||
class={styles.subtitleInput}
|
class={styles.subtitleInput}
|
||||||
placeholder={t('Subheader')}
|
placeholder={t('Subheader')}
|
||||||
initialValue={form.subtitle}
|
initialValue={form.subtitle || ''}
|
||||||
maxLength={MAX_HEADER_LIMIT}
|
maxLength={MAX_HEADER_LIMIT}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -70,10 +70,10 @@ export const PublishSettings = (props: Props) => {
|
||||||
return {
|
return {
|
||||||
coverImageUrl: props.form?.coverImageUrl,
|
coverImageUrl: props.form?.coverImageUrl,
|
||||||
mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
|
mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
|
||||||
slug: props.form?.slug,
|
slug: props.form?.slug || '',
|
||||||
title: props.form?.title,
|
title: props.form?.title || '',
|
||||||
subtitle: props.form?.subtitle,
|
subtitle: props.form?.subtitle || '',
|
||||||
description: composeDescription(),
|
description: composeDescription() || '',
|
||||||
selectedTopics: [],
|
selectedTopics: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -100,7 +100,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
const handleTopicSelectChange = (newSelectedTopics) => {
|
const handleTopicSelectChange = (newSelectedTopics) => {
|
||||||
if (
|
if (
|
||||||
props.form.selectedTopics.length === 0 ||
|
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) => {
|
setSettingsForm((prev) => {
|
||||||
return {
|
return {
|
||||||
|
@ -176,7 +176,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
<div class={styles.mainTopic}>{settingsForm.mainTopic.title}</div>
|
<div class={styles.mainTopic}>{settingsForm.mainTopic.title}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.shoutCardTitle}>{settingsForm.title}</div>
|
<div class={styles.shoutCardTitle}>{settingsForm.title}</div>
|
||||||
<div class={styles.shoutCardSubtitle}>{settingsForm.subtitle}</div>
|
<div class={styles.shoutCardSubtitle}>{settingsForm.subtitle || ''}</div>
|
||||||
<div class={styles.shoutAuthor}>{author()?.name}</div>
|
<div class={styles.shoutAuthor}>{author()?.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -203,7 +203,7 @@ export const PublishSettings = (props: Props) => {
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
fieldName={t('Subheader')}
|
fieldName={t('Subheader')}
|
||||||
placeholder={t('Come up with a subtitle for your story')}
|
placeholder={t('Come up with a subtitle for your story')}
|
||||||
initialValue={settingsForm.subtitle}
|
initialValue={settingsForm.subtitle || ''}
|
||||||
value={(value) => setSettingsForm('subtitle', value)}
|
value={(value) => setSettingsForm('subtitle', value)}
|
||||||
allowEnterKey={false}
|
allowEnterKey={false}
|
||||||
maxLength={100}
|
maxLength={100}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
},
|
},
|
||||||
onmessage(event) {
|
onmessage(event) {
|
||||||
const m: SSEMessage = JSON.parse(event.data)
|
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||||
console.log('[context.connect] Received message:', m)
|
console.log('[context.connect] Received message:', m)
|
||||||
|
|
||||||
// Iterate over all registered handlers and call them
|
// Iterate over all registered handlers and call them
|
||||||
|
|
|
@ -39,7 +39,7 @@ type EditorContextType = {
|
||||||
wordCounter: Accessor<WordCounter>
|
wordCounter: Accessor<WordCounter>
|
||||||
form: ShoutForm
|
form: ShoutForm
|
||||||
formErrors: Record<keyof ShoutForm, string>
|
formErrors: Record<keyof ShoutForm, string>
|
||||||
editorRef: { current: () => Editor }
|
editorRef: { current: () => Editor | null }
|
||||||
saveShout: (form: ShoutForm) => Promise<void>
|
saveShout: (form: ShoutForm) => Promise<void>
|
||||||
saveDraft: (form: ShoutForm) => Promise<void>
|
saveDraft: (form: ShoutForm) => Promise<void>
|
||||||
saveDraftToLocalStorage: (form: ShoutForm) => void
|
saveDraftToLocalStorage: (form: ShoutForm) => void
|
||||||
|
@ -72,7 +72,7 @@ const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
|
||||||
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
|
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
|
||||||
}
|
}
|
||||||
const getDraftFromLocalStorage = (shoutId: number) => {
|
const getDraftFromLocalStorage = (shoutId: number) => {
|
||||||
return JSON.parse(localStorage.getItem(`shout-${shoutId}`))
|
return JSON.parse(localStorage.getItem(`shout-${shoutId}`) || '{}')
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeDraftFromLocalStorage = (shoutId: number) => {
|
const removeDraftFromLocalStorage = (shoutId: number) => {
|
||||||
|
@ -80,13 +80,19 @@ const removeDraftFromLocalStorage = (shoutId: number) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorProvider = (props: { children: JSX.Element }) => {
|
export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
const { t } = useLocalize()
|
const localize = useLocalize()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const { showSnackbar } = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
||||||
const editorRef: { current: () => Editor } = { current: null }
|
const editorRef: { current: () => Editor | null } = { current: () => null }
|
||||||
const [form, setForm] = createStore<ShoutForm>(null)
|
const [form, setForm] = createStore<ShoutForm>({
|
||||||
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
body: '',
|
||||||
|
slug: '',
|
||||||
|
shoutId: 0,
|
||||||
|
title: '',
|
||||||
|
selectedTopics: [],
|
||||||
|
})
|
||||||
|
const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
|
||||||
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
||||||
characters: 0,
|
characters: 0,
|
||||||
words: 0,
|
words: 0,
|
||||||
|
@ -95,13 +101,16 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
const countWords = (value) => setWordCounter(value)
|
const countWords = (value) => setWordCounter(value)
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!form.title) {
|
if (!form.title) {
|
||||||
setFormErrors('title', t('Please, set the article title'))
|
setFormErrors('title', localize?.t('Please, set the article title') || '')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedMedia = JSON.parse(form.media)
|
const parsedMedia = JSON.parse(form.media || '[]')
|
||||||
if (form.layout === 'video' && !parsedMedia[0]) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +119,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const validateSettings = () => {
|
const validateSettings = () => {
|
||||||
if (form.selectedTopics.length === 0) {
|
if (form.selectedTopics.length === 0) {
|
||||||
setFormErrors('selectedTopics', t('Required'))
|
setFormErrors('selectedTopics', localize?.t('Required') || '')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +127,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
|
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
|
||||||
|
if (!formToUpdate.shoutId) {
|
||||||
|
console.error(formToUpdate)
|
||||||
|
return { error: 'not enought data' }
|
||||||
|
}
|
||||||
return await apiClient.updateArticle({
|
return await apiClient.updateArticle({
|
||||||
shout_id: formToUpdate.shoutId,
|
shout_id: formToUpdate.shoutId,
|
||||||
shout_input: {
|
shout_input: {
|
||||||
|
@ -143,48 +156,61 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page().route === 'edit' && !validate()) {
|
if (page()?.route === 'edit' && !validate()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page().route === 'editSettings' && !validateSettings()) {
|
if (page()?.route === 'editSettings' && !validateSettings()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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)
|
removeDraftFromLocalStorage(formToSave.shoutId)
|
||||||
|
|
||||||
if (shout.published_at) {
|
if (shout?.published_at) {
|
||||||
openPage(router, 'article', { slug: shout.slug })
|
openPage(router, 'article', { slug: shout.slug })
|
||||||
} else {
|
} else {
|
||||||
openPage(router, 'drafts')
|
openPage(router, 'drafts')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[saveShout]', error)
|
console.error('[saveShout]', error)
|
||||||
showSnackbar({ type: 'error', body: t('Error') })
|
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDraft = async (draftForm: ShoutForm) => {
|
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) => {
|
const publishShout = async (formToPublish: ShoutForm) => {
|
||||||
if (isEditorPanelVisible()) {
|
const editorPanelVisible = isEditorPanelVisible()
|
||||||
|
const pageRoute = page()?.route
|
||||||
|
|
||||||
|
if (editorPanelVisible) {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page().route === 'edit') {
|
if (pageRoute === 'edit') {
|
||||||
if (!validate()) {
|
if (!validate()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateShout(formToPublish, { publish: false })
|
|
||||||
|
|
||||||
const slug = slugify(form.title)
|
const slug = slugify(form.title)
|
||||||
setForm('slug', slug)
|
setForm('slug', slug)
|
||||||
openPage(router, 'editSettings', { shoutId: form.shoutId.toString() })
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,20 +219,33 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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')
|
openPage(router, 'feed')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[publishShout]', 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) => {
|
const publishShoutById = async (shout_id: number) => {
|
||||||
|
if (!shout_id) {
|
||||||
|
console.error(`shout_id is ${shout_id}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const newShout = await apiClient.updateArticle({
|
const { shout: newShout, error } = await apiClient.updateArticle({
|
||||||
shout_id,
|
shout_id,
|
||||||
publish: true,
|
publish: true,
|
||||||
})
|
})
|
||||||
|
if (error) {
|
||||||
|
console.error(error)
|
||||||
|
snackbar?.showSnackbar({ type: 'error', body: error })
|
||||||
|
return
|
||||||
|
}
|
||||||
if (newShout) {
|
if (newShout) {
|
||||||
addArticles([newShout])
|
addArticles([newShout])
|
||||||
openPage(router, 'feed')
|
openPage(router, 'feed')
|
||||||
|
@ -215,7 +254,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[publishShoutById]', 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
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
showSnackbar({ type: 'error', body: t('Error') })
|
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { createStore, reconcile } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||||
import { useSession } from './session'
|
import { useLocalize } from './localize'
|
||||||
|
import { useSnackbar } from './snackbar'
|
||||||
|
|
||||||
type ReactionsContextType = {
|
type ReactionsContextType = {
|
||||||
reactionEntities: Accessor<Record<number, Reaction>>
|
reactionEntities: Accessor<Record<number, Reaction>>
|
||||||
|
@ -20,7 +21,7 @@ type ReactionsContextType = {
|
||||||
}) => Promise<Reaction[]>
|
}) => Promise<Reaction[]>
|
||||||
createReaction: (reaction: ReactionInput) => Promise<void>
|
createReaction: (reaction: ReactionInput) => Promise<void>
|
||||||
updateReaction: (reaction: ReactionInput) => Promise<Reaction>
|
updateReaction: (reaction: ReactionInput) => Promise<Reaction>
|
||||||
deleteReaction: (id: number) => Promise<void>
|
deleteReaction: (id: number) => Promise<{ error: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionsContext = createContext<ReactionsContextType>()
|
const ReactionsContext = createContext<ReactionsContextType>()
|
||||||
|
@ -30,8 +31,9 @@ export function useReactions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
const [reactionEntities, setReactionEntities] = createSignal<Record<number, Reaction> | undefined>()
|
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
|
||||||
const { author } = useSession()
|
const { t } = useLocalize()
|
||||||
|
const { showSnackbar } = useSnackbar()
|
||||||
|
|
||||||
const loadReactionsBy = async ({
|
const loadReactionsBy = async ({
|
||||||
by,
|
by,
|
||||||
|
@ -55,18 +57,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const createReaction = async (input: ReactionInput): Promise<void> => {
|
const createReaction = async (input: ReactionInput): Promise<void> => {
|
||||||
const fakeId = Date.now() + Math.floor(Math.random() * 1000)
|
const { error, reaction } = await apiClient.createReaction(input)
|
||||||
setReactionEntities((rrr: Record<number, Reaction>) => ({
|
if (error) await showSnackbar({ type: 'error', body: t(error) })
|
||||||
...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 })
|
|
||||||
if (!reaction) return
|
if (!reaction) return
|
||||||
const changes = {
|
const changes = {
|
||||||
[reaction.id]: reaction,
|
[reaction.id]: reaction,
|
||||||
|
@ -92,19 +84,22 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
setReactionEntities(changes)
|
setReactionEntities(changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteReaction = async (reaction: number): Promise<void> => {
|
const deleteReaction = async (reaction_id: number): Promise<{ error: string; reaction?: string }> => {
|
||||||
setReactionEntities({ [reaction]: undefined })
|
if (reaction_id) {
|
||||||
await apiClient.destroyReaction(reaction)
|
const result = await apiClient.destroyReaction(reaction_id)
|
||||||
|
if (!result.error) {
|
||||||
|
setReactionEntities({
|
||||||
|
[reaction_id]: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReaction = async (input: ReactionInput): Promise<Reaction> => {
|
const updateReaction = async (input: ReactionInput): Promise<Reaction> => {
|
||||||
const reaction = await apiClient.updateReaction(input)
|
const { error, reaction } = await apiClient.updateReaction(input)
|
||||||
if (reaction) {
|
if (error) await showSnackbar({ type: 'error', body: t(error) })
|
||||||
setReactionEntities((rrr) => {
|
if (reaction) setReactionEntities(reaction.id, reaction)
|
||||||
rrr[reaction.id] = reaction
|
|
||||||
return rrr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return reaction
|
return reaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import reactionDestroy from '../mutation/core/reaction-destroy'
|
||||||
import reactionUpdate from '../mutation/core/reaction-update'
|
import reactionUpdate from '../mutation/core/reaction-update'
|
||||||
import unfollowMutation from '../mutation/core/unfollow'
|
import unfollowMutation from '../mutation/core/unfollow'
|
||||||
import shoutLoad from '../query/core/article-load'
|
import shoutLoad from '../query/core/article-load'
|
||||||
|
import getMyShout from '../query/core/article-my'
|
||||||
import shoutsLoadBy from '../query/core/articles-load-by'
|
import shoutsLoadBy from '../query/core/articles-load-by'
|
||||||
import draftsLoad from '../query/core/articles-load-drafts'
|
import draftsLoad from '../query/core/articles-load-drafts'
|
||||||
import myFeed from '../query/core/articles-load-feed'
|
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 authorId from '../query/core/author-id'
|
||||||
import authorsAll from '../query/core/authors-all'
|
import authorsAll from '../query/core/authors-all'
|
||||||
import authorsLoadBy from '../query/core/authors-load-by'
|
import authorsLoadBy from '../query/core/authors-load-by'
|
||||||
import mySubscriptions from '../query/core/my-followed'
|
|
||||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||||
import topicBySlug from '../query/core/topic-by-slug'
|
import topicBySlug from '../query/core/topic-by-slug'
|
||||||
import topicsAll from '../query/core/topics-all'
|
import topicsAll from '../query/core/topics-all'
|
||||||
|
@ -135,7 +135,6 @@ export const apiClient = {
|
||||||
user?: string
|
user?: string
|
||||||
}): Promise<AuthorFollows> => {
|
}): Promise<AuthorFollows> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||||
console.log('!!! response:', response)
|
|
||||||
return response.data.get_author_follows
|
return response.data.get_author_follows
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -162,12 +161,12 @@ export const apiClient = {
|
||||||
shout_id: number
|
shout_id: number
|
||||||
shout_input?: ShoutInput
|
shout_input?: ShoutInput
|
||||||
publish: boolean
|
publish: boolean
|
||||||
}): Promise<Shout> => {
|
}): Promise<CommonResult> => {
|
||||||
const response = await apiClient.private
|
const response = await apiClient.private
|
||||||
.mutation(updateArticle, { shout_id, shout_input, publish })
|
.mutation(updateArticle, { shout_id, shout_input, publish })
|
||||||
.toPromise()
|
.toPromise()
|
||||||
console.debug('[graphql.client.core] updateArticle:', response.data)
|
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<void> => {
|
deleteShout: async (params: MutationDelete_ShoutArgs): Promise<void> => {
|
||||||
|
@ -178,7 +177,7 @@ export const apiClient = {
|
||||||
getDrafts: async (): Promise<Shout[]> => {
|
getDrafts: async (): Promise<Shout[]> => {
|
||||||
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
||||||
console.debug('[graphql.client.core] getDrafts:', response)
|
console.debug('[graphql.client.core] getDrafts:', response)
|
||||||
return response.data.load_shouts_drafts
|
return response.data.get_shouts_drafts
|
||||||
},
|
},
|
||||||
createReaction: async (input: ReactionInput) => {
|
createReaction: async (input: ReactionInput) => {
|
||||||
const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise()
|
const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise()
|
||||||
|
@ -188,7 +187,7 @@ export const apiClient = {
|
||||||
destroyReaction: async (reaction_id: number) => {
|
destroyReaction: async (reaction_id: number) => {
|
||||||
const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise()
|
const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise()
|
||||||
console.debug('[graphql.client.core] destroyReaction:', response)
|
console.debug('[graphql.client.core] destroyReaction:', response)
|
||||||
return response.data.delete_reaction.reaction
|
return response.data.delete_reaction
|
||||||
},
|
},
|
||||||
updateReaction: async (reaction: ReactionInput) => {
|
updateReaction: async (reaction: ReactionInput) => {
|
||||||
const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise()
|
const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise()
|
||||||
|
@ -200,15 +199,18 @@ export const apiClient = {
|
||||||
console.debug('[graphql.client.core] authorsLoadBy:', resp)
|
console.debug('[graphql.client.core] authorsLoadBy:', resp)
|
||||||
return resp.data.load_authors_by
|
return resp.data.load_authors_by
|
||||||
},
|
},
|
||||||
|
|
||||||
getShoutBySlug: async (slug: string) => {
|
getShoutBySlug: async (slug: string) => {
|
||||||
const resp = await publicGraphQLClient.query(shoutLoad, { slug }).toPromise()
|
const resp = await publicGraphQLClient.query(shoutLoad, { slug }).toPromise()
|
||||||
return resp.data.get_shout
|
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)
|
if (resp.error) console.error(resp)
|
||||||
|
|
||||||
return resp.data.get_shout
|
return resp.data.get_my_shout
|
||||||
},
|
},
|
||||||
|
|
||||||
getShouts: async (options: LoadShoutsOptions) => {
|
getShouts: async (options: LoadShoutsOptions) => {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadShoutQuery($slug: String, $shout_id: Int) {
|
query LoadShoutQuery($slug: String!) {
|
||||||
get_shout(slug: $slug, shout_id: $shout_id) {
|
get_shout(slug: $slug) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
lead
|
lead
|
||||||
|
|
53
src/graphql/query/core/article-my.ts
Normal file
53
src/graphql/query/core/article-my.ts
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -2,7 +2,7 @@ import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query LoadDraftsQuery {
|
query LoadDraftsQuery {
|
||||||
load_shouts_drafts {
|
get_shouts_drafts {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
subtitle
|
subtitle
|
||||||
|
@ -35,7 +35,6 @@ export default gql`
|
||||||
featured_at
|
featured_at
|
||||||
stat {
|
stat {
|
||||||
viewed
|
viewed
|
||||||
|
|
||||||
rating
|
rating
|
||||||
commented
|
commented
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default gql`
|
||||||
shouts
|
shouts
|
||||||
authors
|
authors
|
||||||
followers
|
followers
|
||||||
|
comments
|
||||||
# viewed
|
# viewed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,22 +7,42 @@ import { useLocalize } from '../context/localize'
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Shout } from '../graphql/schema/core.gen'
|
import { Shout } from '../graphql/schema/core.gen'
|
||||||
import { useRouter } from '../stores/router'
|
import { useRouter } from '../stores/router'
|
||||||
|
import { router } from '../stores/router'
|
||||||
|
|
||||||
|
import { redirectPage } from '@nanostores/router'
|
||||||
|
import { useSnackbar } from '../context/snackbar'
|
||||||
import { LayoutType } from './types'
|
import { LayoutType } from './types'
|
||||||
|
|
||||||
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
||||||
|
|
||||||
export const EditPage = () => {
|
export const EditPage = () => {
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
const snackbar = useSnackbar()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const shoutId = createMemo(() => Number((page().params as Record<'shoutId', string>).shoutId))
|
|
||||||
|
|
||||||
const [shout, setShout] = createSignal<Shout>(null)
|
const [shout, setShout] = createSignal<Shout>(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 () => {
|
onMount(async () => {
|
||||||
const loadedShout = await apiClient.getShoutById(shoutId())
|
const shout_id = window.location.pathname.split('/').pop()
|
||||||
setShout(loadedShout)
|
if (shout_id) {
|
||||||
|
try {
|
||||||
|
await loadMyShout(parseInt(shout_id, 10))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
|
|
|
@ -464,7 +464,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-message--error {
|
.form-message--error {
|
||||||
color: #d00820;
|
color: var(--danger-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -7,7 +7,7 @@ export const byCreated = (a: Shout | Reaction, b: Shout | Reaction) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const byPublished = (a: Shout, b: Shout) => {
|
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 = (
|
export const byLength = (
|
||||||
|
|
Loading…
Reference in New Issue
Block a user