Merge pull request #414 from Discours/hotfix/posting-author

posting author fixes
This commit is contained in:
Tony 2024-03-07 15:35:30 +03:00 committed by GitHub
commit ae589e39fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 258 additions and 97 deletions

1
.gitignore vendored
View File

@ -22,3 +22,4 @@ bun.lockb
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
/plawright-report/ /plawright-report/
target

View File

@ -378,6 +378,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",
@ -524,6 +525,7 @@
"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", "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" "Restore password": "Restore password"
} }

View File

@ -149,6 +149,7 @@
"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": "Ошибка",
"Experience": "Личный опыт", "Experience": "Личный опыт",
"FAQ": "Советы и предложения", "FAQ": "Советы и предложения",
@ -529,6 +530,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": "зарегистрироваться или войти",
@ -550,6 +552,7 @@
"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": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля", "It's OK. Just enter your email to receive a link to change your password": "Ничего страшного. Просто укажите свою почту, чтобы получить ссылку для смены пароля",
"Restore password": "Восстановить пароль" "Restore password": "Восстановить пароль"
} }

View File

@ -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)
} }
} }

View File

@ -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 []
} }

View File

@ -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>

View File

@ -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>

View File

@ -48,7 +48,6 @@ export const RegisterForm = () => {
} }
const handleSubmit = async (event: Event) => { const handleSubmit = async (event: Event) => {
console.log('!!! handleSubmit:', handleSubmit)
event.preventDefault() event.preventDefault()
if (passwordError()) { if (passwordError()) {
setValidationErrors((errors) => ({ ...errors, password: passwordError() })) setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
@ -148,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>
</> </>
), ),
@ -193,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()}>
@ -226,8 +226,8 @@ export const RegisterForm = () => {
<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>

View File

@ -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}
@ -98,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()}>

View File

@ -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>

View File

@ -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}

View File

@ -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

View File

@ -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
} }
} }

View File

@ -5,6 +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 { useLocalize } from './localize'
import { useSnackbar } from './snackbar'
type ReactionsContextType = { type ReactionsContextType = {
reactionEntities: Record<number, Reaction> reactionEntities: Record<number, Reaction>
@ -19,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,6 +32,8 @@ export function useReactions() {
export const ReactionsProvider = (props: { children: JSX.Element }) => { export const ReactionsProvider = (props: { children: JSX.Element }) => {
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({}) const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
const { t } = useLocalize()
const { showSnackbar } = useSnackbar()
const loadReactionsBy = async ({ const loadReactionsBy = async ({
by, by,
@ -53,7 +57,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
} }
const createReaction = async (input: ReactionInput): Promise<void> => { const createReaction = async (input: ReactionInput): Promise<void> => {
const reaction = await apiClient.createReaction(input) const { error, reaction } = await apiClient.createReaction(input)
if (error) await showSnackbar({ type: 'error', body: t(error) })
if (!reaction) return if (!reaction) return
const changes = { const changes = {
[reaction.id]: reaction, [reaction.id]: reaction,
@ -79,18 +84,22 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
setReactionEntities(changes) setReactionEntities(changes)
} }
const deleteReaction = async (reaction_id: number): Promise<void> => { const deleteReaction = async (reaction_id: number): Promise<{ error: string; reaction?: string }> => {
if (reaction_id) { if (reaction_id) {
await apiClient.destroyReaction(reaction_id) const result = await apiClient.destroyReaction(reaction_id)
if (!result.error) {
setReactionEntities({ setReactionEntities({
[reaction_id]: undefined, [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)
setReactionEntities(reaction.id, reaction) if (error) await showSnackbar({ type: 'error', body: t(error) })
if (reaction) setReactionEntities(reaction.id, reaction)
return reaction return reaction
} }

View File

@ -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) => {

View File

@ -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

View 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
}
}
}
`

View File

@ -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
} }

View File

@ -13,6 +13,7 @@ export default gql`
shouts shouts
authors authors
followers followers
comments
# viewed # viewed
} }
} }

View File

@ -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(() => {

View File

@ -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 = (