Merge pull request #414 from Discours/hotfix/posting-author
posting author fixes
This commit is contained in:
commit
ae589e39fa
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -22,3 +22,4 @@ bun.lockb
|
|||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/plawright-report/
|
||||
target
|
|
@ -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 publishing settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?",
|
||||
"This comment has not yet been rated": "This comment has not yet been rated",
|
||||
"This content is not published yet": "This content is not published yet",
|
||||
"This email is": "This email is",
|
||||
"This email is not verified": "This email is not verified",
|
||||
"This email is verified": "This email is verified",
|
||||
|
@ -524,6 +525,7 @@
|
|||
"view": "view",
|
||||
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
|
||||
"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,6 +149,7 @@
|
|||
"Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
|
||||
"Enter your new password": "Введите новый пароль",
|
||||
"Enter": "Войти",
|
||||
"This content is not published yet": "Содержимое ещё не опубликовано",
|
||||
"Error": "Ошибка",
|
||||
"Experience": "Личный опыт",
|
||||
"FAQ": "Советы и предложения",
|
||||
|
@ -529,6 +530,7 @@
|
|||
"repeat": "повторить",
|
||||
"resend confirmation link": "отправить ссылку ещё раз",
|
||||
"shout": "пост",
|
||||
"shout not found": "публикация не найдена",
|
||||
"shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}",
|
||||
"sign in": "войти",
|
||||
"sign up or sign in": "зарегистрироваться или войти",
|
||||
|
@ -550,6 +552,7 @@
|
|||
"view": "просмотр",
|
||||
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
|
||||
"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) {
|
||||
await deleteReaction(props.comment.id)
|
||||
// TODO: Учесть то что deleteReaction может вернуть error
|
||||
if (props.onDelete) {
|
||||
const { error } = await deleteReaction(props.comment.id)
|
||||
const notificationType = error ? 'error' : 'success'
|
||||
const notificationMessage = error
|
||||
? t('Failed to delete comment')
|
||||
: t('Comment successfully deleted')
|
||||
await showSnackbar({ type: notificationType, body: notificationMessage })
|
||||
|
||||
if (!error && props.onDelete) {
|
||||
props.onDelete(props.comment.id)
|
||||
}
|
||||
await showSnackbar({ body: t('Comment successfully deleted') })
|
||||
}
|
||||
} catch (error) {
|
||||
await showSnackbar({ body: 'error' })
|
||||
console.error('[deleteReaction]', error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,7 +141,7 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
const media = createMemo<MediaItem[]>(() => {
|
||||
try {
|
||||
return JSON.parse(props.article.media)
|
||||
return JSON.parse(props.article?.media || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
|
|
@ -67,19 +67,19 @@ const getTitleAndSubtitle = (
|
|||
subtitle: string
|
||||
} => {
|
||||
let title = article.title
|
||||
let subtitle = article.subtitle
|
||||
let subtitle: string = article.subtitle || ''
|
||||
|
||||
if (!subtitle) {
|
||||
let tt = article.title?.split('. ') || []
|
||||
let titleParts = article.title?.split('. ') || []
|
||||
|
||||
if (tt?.length === 1) {
|
||||
tt = article.title?.split(/{!|\?|:|;}\s/) || []
|
||||
if (titleParts?.length === 1) {
|
||||
titleParts = article.title?.split(/{!|\?|:|;}\s/) || []
|
||||
}
|
||||
|
||||
if (tt && tt.length > 1) {
|
||||
const sep = article.title?.replace(tt[0], '').split(' ', 1)[0]
|
||||
title = tt[0] + (sep === '.' || sep === ':' ? '' : sep)
|
||||
subtitle = capitalize(article.title?.replace(tt[0] + sep, ''), true)
|
||||
if (titleParts && titleParts.length > 1) {
|
||||
const sep = article.title?.replace(titleParts[0], '').split(' ', 1)[0]
|
||||
title = titleParts[0] + (sep === '.' || sep === ':' ? '' : sep)
|
||||
subtitle = capitalize(article.title?.replace(titleParts[0] + sep, ''), true) || ''
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +117,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
const { title, subtitle } = getTitleAndSubtitle(props.article)
|
||||
|
||||
const formattedDate = createMemo<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(
|
||||
|
@ -135,6 +135,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
scrollTo: 'comments',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
|
||||
|
@ -153,7 +154,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
[aspectRatio()]: props.withAspectRatio,
|
||||
})}
|
||||
>
|
||||
{/* Cover Image */}
|
||||
<Show when={!(props.settings?.noimage || props.settings?.isFeedMode)}>
|
||||
{/* Cover Image Container */}
|
||||
<div class={styles.shoutCardCoverContainer}>
|
||||
<div
|
||||
class={clsx(styles.shoutCardCover, {
|
||||
|
@ -178,7 +181,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Shout Card Content */}
|
||||
<div class={styles.shoutCardContent}>
|
||||
{/* Shout Card Icon */}
|
||||
<Show
|
||||
when={
|
||||
props.article.layout &&
|
||||
|
@ -195,6 +201,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Main Topic */}
|
||||
<Show when={!props.settings?.isGroup && mainTopicSlug}>
|
||||
<CardTopic
|
||||
title={mainTopicTitle}
|
||||
|
@ -205,6 +212,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
/>
|
||||
</Show>
|
||||
|
||||
{/* Title and Subtitle */}
|
||||
<div
|
||||
class={clsx(styles.shoutCardTitlesContainer, {
|
||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
|
||||
|
@ -224,22 +232,23 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</Show>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
|
||||
{/* Author and Date */}
|
||||
<div
|
||||
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
|
||||
>
|
||||
<Show when={!props.settings?.noauthor}>
|
||||
<div class={styles.shoutAuthor}>
|
||||
<For each={props.article.authors}>
|
||||
{(a: Author) => {
|
||||
return (
|
||||
{(a: Author) => (
|
||||
<AuthorLink
|
||||
size={'XS'}
|
||||
author={a}
|
||||
isFloorImportant={props.settings.isFloorImportant || props.settings?.isWithCover}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -248,6 +257,8 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Description */}
|
||||
<Show when={props.article.description}>
|
||||
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
||||
</Show>
|
||||
|
|
|
@ -51,7 +51,15 @@ const DialogAvatar = (props: Props) => {
|
|||
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
|
|
|
@ -48,7 +48,6 @@ export const RegisterForm = () => {
|
|||
}
|
||||
|
||||
const handleSubmit = async (event: Event) => {
|
||||
console.log('!!! handleSubmit:', handleSubmit)
|
||||
event.preventDefault()
|
||||
if (passwordError()) {
|
||||
setValidationErrors((errors) => ({ ...errors, password: passwordError() }))
|
||||
|
@ -148,9 +147,10 @@ export const RegisterForm = () => {
|
|||
...prev,
|
||||
email: (
|
||||
<>
|
||||
{t('This email is registered')}. {t('You can')}{' '}
|
||||
{t('This email is registered')}
|
||||
{'. '}
|
||||
<span class="link" onClick={() => changeSearchParams({ mode: 'send-reset-link' })}>
|
||||
{t('Set the new password').toLocaleLowerCase()}
|
||||
{t('Set the new password')}
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
|
@ -193,7 +193,7 @@ export const RegisterForm = () => {
|
|||
disabled={Boolean(emailStatus())}
|
||||
placeholder={t('Full name')}
|
||||
autocomplete="one-time-code"
|
||||
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
||||
onChange={(event) => handleNameInput(event.currentTarget.value)}
|
||||
/>
|
||||
<label for="name">{t('Full name')}</label>
|
||||
<Show when={validationErrors().fullName && !emailStatus()}>
|
||||
|
@ -226,8 +226,8 @@ export const RegisterForm = () => {
|
|||
<PasswordField
|
||||
disableAutocomplete={true}
|
||||
disabled={Boolean(emailStatus())}
|
||||
errorMessage={(err) => setPasswordError(err)}
|
||||
onInput={(value) => setPassword(value)}
|
||||
errorMessage={(err) => !emailStatus() && setPasswordError(err)}
|
||||
onInput={(value) => setPassword(emailStatus() ? '' : value)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { AuthModalSearchParams } from './types'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { JSX, Show, createSignal } from 'solid-js'
|
||||
import { JSX, Show, createSignal, onMount } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
|
@ -72,6 +72,12 @@ export const SendResetLinkForm = () => {
|
|||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (email()) {
|
||||
console.info('[SendResetLinkForm] email detected')
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
|
@ -98,7 +104,7 @@ export const SendResetLinkForm = () => {
|
|||
type="email"
|
||||
value={email()}
|
||||
placeholder={t('Email')}
|
||||
onInput={(event) => handleEmailInput(event.currentTarget.value)}
|
||||
onChange={(event) => handleEmailInput(event.currentTarget.value)}
|
||||
/>
|
||||
<label for="email">{t('Email')}</label>
|
||||
<Show when={isUserNotFound()}>
|
||||
|
|
|
@ -64,10 +64,11 @@ export const EditView = (props: Props) => {
|
|||
getDraftFromLocalStorage,
|
||||
} = useEditorContext()
|
||||
const shoutTopics = props.shout.topics || []
|
||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||
|
||||
// TODO: проверить сохранение черновика в local storage (не работает)
|
||||
const draft = getDraftFromLocalStorage(props.shout.id)
|
||||
if (draft) {
|
||||
setForm(draft)
|
||||
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id })
|
||||
} else {
|
||||
setForm({
|
||||
slug: props.shout.slug,
|
||||
|
@ -179,6 +180,7 @@ export const EditView = (props: Props) => {
|
|||
|
||||
let autoSaveTimeOutId: number | string | NodeJS.Timeout
|
||||
|
||||
//TODO: add throttle
|
||||
const autoSaveRecursive = () => {
|
||||
autoSaveTimeOutId = setTimeout(async () => {
|
||||
const hasChanges = !deepEqual(form, prevForm)
|
||||
|
@ -307,10 +309,10 @@ export const EditView = (props: Props) => {
|
|||
subtitleInput.current = el
|
||||
}}
|
||||
allowEnterKey={false}
|
||||
value={(value) => setForm('subtitle', value)}
|
||||
value={(value) => setForm('subtitle', value || '')}
|
||||
class={styles.subtitleInput}
|
||||
placeholder={t('Subheader')}
|
||||
initialValue={form.subtitle}
|
||||
initialValue={form.subtitle || ''}
|
||||
maxLength={MAX_HEADER_LIMIT}
|
||||
/>
|
||||
</Show>
|
||||
|
|
|
@ -70,10 +70,10 @@ export const PublishSettings = (props: Props) => {
|
|||
return {
|
||||
coverImageUrl: props.form?.coverImageUrl,
|
||||
mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
|
||||
slug: props.form?.slug,
|
||||
title: props.form?.title,
|
||||
subtitle: props.form?.subtitle,
|
||||
description: composeDescription(),
|
||||
slug: props.form?.slug || '',
|
||||
title: props.form?.title || '',
|
||||
subtitle: props.form?.subtitle || '',
|
||||
description: composeDescription() || '',
|
||||
selectedTopics: [],
|
||||
}
|
||||
})
|
||||
|
@ -100,7 +100,7 @@ export const PublishSettings = (props: Props) => {
|
|||
const handleTopicSelectChange = (newSelectedTopics) => {
|
||||
if (
|
||||
props.form.selectedTopics.length === 0 ||
|
||||
newSelectedTopics.every((topic) => topic.id !== props.form.mainTopic.id)
|
||||
newSelectedTopics.every((topic) => topic.id !== props.form.mainTopic?.id)
|
||||
) {
|
||||
setSettingsForm((prev) => {
|
||||
return {
|
||||
|
@ -176,7 +176,7 @@ export const PublishSettings = (props: Props) => {
|
|||
<div class={styles.mainTopic}>{settingsForm.mainTopic.title}</div>
|
||||
</Show>
|
||||
<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>
|
||||
</div>
|
||||
|
@ -203,7 +203,7 @@ export const PublishSettings = (props: Props) => {
|
|||
variant="bordered"
|
||||
fieldName={t('Subheader')}
|
||||
placeholder={t('Come up with a subtitle for your story')}
|
||||
initialValue={settingsForm.subtitle}
|
||||
initialValue={settingsForm.subtitle || ''}
|
||||
value={(value) => setSettingsForm('subtitle', value)}
|
||||
allowEnterKey={false}
|
||||
maxLength={100}
|
||||
|
|
|
@ -50,7 +50,7 @@ export const ConnectProvider = (props: { children: JSX.Element }) => {
|
|||
Authorization: token,
|
||||
},
|
||||
onmessage(event) {
|
||||
const m: SSEMessage = JSON.parse(event.data)
|
||||
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||
console.log('[context.connect] Received message:', m)
|
||||
|
||||
// Iterate over all registered handlers and call them
|
||||
|
|
|
@ -39,7 +39,7 @@ type EditorContextType = {
|
|||
wordCounter: Accessor<WordCounter>
|
||||
form: ShoutForm
|
||||
formErrors: Record<keyof ShoutForm, string>
|
||||
editorRef: { current: () => Editor }
|
||||
editorRef: { current: () => Editor | null }
|
||||
saveShout: (form: ShoutForm) => Promise<void>
|
||||
saveDraft: (form: ShoutForm) => Promise<void>
|
||||
saveDraftToLocalStorage: (form: ShoutForm) => void
|
||||
|
@ -72,7 +72,7 @@ const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
|
|||
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
|
||||
}
|
||||
const getDraftFromLocalStorage = (shoutId: number) => {
|
||||
return JSON.parse(localStorage.getItem(`shout-${shoutId}`))
|
||||
return JSON.parse(localStorage.getItem(`shout-${shoutId}`) || '{}')
|
||||
}
|
||||
|
||||
const removeDraftFromLocalStorage = (shoutId: number) => {
|
||||
|
@ -80,13 +80,19 @@ const removeDraftFromLocalStorage = (shoutId: number) => {
|
|||
}
|
||||
|
||||
export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||
const { t } = useLocalize()
|
||||
const localize = useLocalize()
|
||||
const { page } = useRouter()
|
||||
const { showSnackbar } = useSnackbar()
|
||||
const snackbar = useSnackbar()
|
||||
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
|
||||
const editorRef: { current: () => Editor } = { current: null }
|
||||
const [form, setForm] = createStore<ShoutForm>(null)
|
||||
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
||||
const editorRef: { current: () => Editor | null } = { current: () => null }
|
||||
const [form, setForm] = createStore<ShoutForm>({
|
||||
body: '',
|
||||
slug: '',
|
||||
shoutId: 0,
|
||||
title: '',
|
||||
selectedTopics: [],
|
||||
})
|
||||
const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
|
||||
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
||||
characters: 0,
|
||||
words: 0,
|
||||
|
@ -95,13 +101,16 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
const countWords = (value) => setWordCounter(value)
|
||||
const validate = () => {
|
||||
if (!form.title) {
|
||||
setFormErrors('title', t('Please, set the article title'))
|
||||
setFormErrors('title', localize?.t('Please, set the article title') || '')
|
||||
return false
|
||||
}
|
||||
|
||||
const parsedMedia = JSON.parse(form.media)
|
||||
const parsedMedia = JSON.parse(form.media || '[]')
|
||||
if (form.layout === 'video' && !parsedMedia[0]) {
|
||||
showSnackbar({ type: 'error', body: t('Looks like you forgot to upload the video') })
|
||||
snackbar?.showSnackbar({
|
||||
type: 'error',
|
||||
body: localize?.t('Looks like you forgot to upload the video'),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -110,7 +119,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
|
||||
const validateSettings = () => {
|
||||
if (form.selectedTopics.length === 0) {
|
||||
setFormErrors('selectedTopics', t('Required'))
|
||||
setFormErrors('selectedTopics', localize?.t('Required') || '')
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -118,6 +127,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
|
||||
if (!formToUpdate.shoutId) {
|
||||
console.error(formToUpdate)
|
||||
return { error: 'not enought data' }
|
||||
}
|
||||
return await apiClient.updateArticle({
|
||||
shout_id: formToUpdate.shoutId,
|
||||
shout_input: {
|
||||
|
@ -143,48 +156,61 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
toggleEditorPanel()
|
||||
}
|
||||
|
||||
if (page().route === 'edit' && !validate()) {
|
||||
if (page()?.route === 'edit' && !validate()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (page().route === 'editSettings' && !validateSettings()) {
|
||||
if (page()?.route === 'editSettings' && !validateSettings()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const shout = await updateShout(formToSave, { publish: false })
|
||||
const { shout, error } = await updateShout(formToSave, { publish: false })
|
||||
if (error) {
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' })
|
||||
return
|
||||
}
|
||||
removeDraftFromLocalStorage(formToSave.shoutId)
|
||||
|
||||
if (shout.published_at) {
|
||||
if (shout?.published_at) {
|
||||
openPage(router, 'article', { slug: shout.slug })
|
||||
} else {
|
||||
openPage(router, 'drafts')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[saveShout]', error)
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
|
||||
}
|
||||
}
|
||||
|
||||
const saveDraft = async (draftForm: ShoutForm) => {
|
||||
await updateShout(draftForm, { publish: false })
|
||||
const { error } = await updateShout(draftForm, { publish: false })
|
||||
if (error) {
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const publishShout = async (formToPublish: ShoutForm) => {
|
||||
if (isEditorPanelVisible()) {
|
||||
const editorPanelVisible = isEditorPanelVisible()
|
||||
const pageRoute = page()?.route
|
||||
|
||||
if (editorPanelVisible) {
|
||||
toggleEditorPanel()
|
||||
}
|
||||
|
||||
if (page().route === 'edit') {
|
||||
if (pageRoute === 'edit') {
|
||||
if (!validate()) {
|
||||
return
|
||||
}
|
||||
|
||||
await updateShout(formToPublish, { publish: false })
|
||||
|
||||
const slug = slugify(form.title)
|
||||
setForm('slug', slug)
|
||||
openPage(router, 'editSettings', { shoutId: form.shoutId.toString() })
|
||||
const { error } = await updateShout(formToPublish, { publish: false })
|
||||
if (error) {
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -193,20 +219,33 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
try {
|
||||
await updateShout(formToPublish, { publish: true })
|
||||
const { error } = await updateShout(formToPublish, { publish: true })
|
||||
if (error) {
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' })
|
||||
return
|
||||
}
|
||||
openPage(router, 'feed')
|
||||
} catch (error) {
|
||||
console.error('[publishShout]', error)
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
|
||||
}
|
||||
}
|
||||
|
||||
const publishShoutById = async (shout_id: number) => {
|
||||
if (!shout_id) {
|
||||
console.error(`shout_id is ${shout_id}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const newShout = await apiClient.updateArticle({
|
||||
const { shout: newShout, error } = await apiClient.updateArticle({
|
||||
shout_id,
|
||||
publish: true,
|
||||
})
|
||||
if (error) {
|
||||
console.error(error)
|
||||
snackbar?.showSnackbar({ type: 'error', body: error })
|
||||
return
|
||||
}
|
||||
if (newShout) {
|
||||
addArticles([newShout])
|
||||
openPage(router, 'feed')
|
||||
|
@ -215,7 +254,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('[publishShoutById]', error)
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,7 +265,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
|||
})
|
||||
return true
|
||||
} catch {
|
||||
showSnackbar({ type: 'error', body: t('Error') })
|
||||
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import { createStore, reconcile } from 'solid-js/store'
|
|||
|
||||
import { apiClient } from '../graphql/client/core'
|
||||
import { Reaction, ReactionBy, ReactionInput, ReactionKind } from '../graphql/schema/core.gen'
|
||||
import { useLocalize } from './localize'
|
||||
import { useSnackbar } from './snackbar'
|
||||
|
||||
type ReactionsContextType = {
|
||||
reactionEntities: Record<number, Reaction>
|
||||
|
@ -19,7 +21,7 @@ type ReactionsContextType = {
|
|||
}) => Promise<Reaction[]>
|
||||
createReaction: (reaction: ReactionInput) => Promise<void>
|
||||
updateReaction: (reaction: ReactionInput) => Promise<Reaction>
|
||||
deleteReaction: (id: number) => Promise<void>
|
||||
deleteReaction: (id: number) => Promise<{ error: string }>
|
||||
}
|
||||
|
||||
const ReactionsContext = createContext<ReactionsContextType>()
|
||||
|
@ -30,6 +32,8 @@ export function useReactions() {
|
|||
|
||||
export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||
const [reactionEntities, setReactionEntities] = createStore<Record<number, Reaction>>({})
|
||||
const { t } = useLocalize()
|
||||
const { showSnackbar } = useSnackbar()
|
||||
|
||||
const loadReactionsBy = async ({
|
||||
by,
|
||||
|
@ -53,7 +57,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
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
|
||||
const changes = {
|
||||
[reaction.id]: reaction,
|
||||
|
@ -79,18 +84,22 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
setReactionEntities(changes)
|
||||
}
|
||||
|
||||
const deleteReaction = async (reaction_id: number): Promise<void> => {
|
||||
const deleteReaction = async (reaction_id: number): Promise<{ error: string; reaction?: string }> => {
|
||||
if (reaction_id) {
|
||||
await apiClient.destroyReaction(reaction_id)
|
||||
const result = await apiClient.destroyReaction(reaction_id)
|
||||
if (!result.error) {
|
||||
setReactionEntities({
|
||||
[reaction_id]: undefined,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const updateReaction = async (input: ReactionInput): Promise<Reaction> => {
|
||||
const reaction = await apiClient.updateReaction(input)
|
||||
setReactionEntities(reaction.id, reaction)
|
||||
const { error, reaction } = await apiClient.updateReaction(input)
|
||||
if (error) await showSnackbar({ type: 'error', body: t(error) })
|
||||
if (reaction) setReactionEntities(reaction.id, reaction)
|
||||
return reaction
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import reactionDestroy from '../mutation/core/reaction-destroy'
|
|||
import reactionUpdate from '../mutation/core/reaction-update'
|
||||
import unfollowMutation from '../mutation/core/unfollow'
|
||||
import shoutLoad from '../query/core/article-load'
|
||||
import getMyShout from '../query/core/article-my'
|
||||
import shoutsLoadBy from '../query/core/articles-load-by'
|
||||
import draftsLoad from '../query/core/articles-load-drafts'
|
||||
import myFeed from '../query/core/articles-load-feed'
|
||||
|
@ -41,7 +42,6 @@ import authorFollows from '../query/core/author-follows'
|
|||
import authorId from '../query/core/author-id'
|
||||
import authorsAll from '../query/core/authors-all'
|
||||
import authorsLoadBy from '../query/core/authors-load-by'
|
||||
import mySubscriptions from '../query/core/my-followed'
|
||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||
import topicBySlug from '../query/core/topic-by-slug'
|
||||
import topicsAll from '../query/core/topics-all'
|
||||
|
@ -135,7 +135,6 @@ export const apiClient = {
|
|||
user?: string
|
||||
}): Promise<AuthorFollows> => {
|
||||
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||
console.log('!!! response:', response)
|
||||
return response.data.get_author_follows
|
||||
},
|
||||
|
||||
|
@ -162,12 +161,12 @@ export const apiClient = {
|
|||
shout_id: number
|
||||
shout_input?: ShoutInput
|
||||
publish: boolean
|
||||
}): Promise<Shout> => {
|
||||
}): Promise<CommonResult> => {
|
||||
const response = await apiClient.private
|
||||
.mutation(updateArticle, { shout_id, shout_input, publish })
|
||||
.toPromise()
|
||||
console.debug('[graphql.client.core] updateArticle:', response.data)
|
||||
return response.data.update_shout.shout
|
||||
return response.data.update_shout
|
||||
},
|
||||
|
||||
deleteShout: async (params: MutationDelete_ShoutArgs): Promise<void> => {
|
||||
|
@ -178,7 +177,7 @@ export const apiClient = {
|
|||
getDrafts: async (): Promise<Shout[]> => {
|
||||
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
||||
console.debug('[graphql.client.core] getDrafts:', response)
|
||||
return response.data.load_shouts_drafts
|
||||
return response.data.get_shouts_drafts
|
||||
},
|
||||
createReaction: async (input: ReactionInput) => {
|
||||
const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise()
|
||||
|
@ -188,7 +187,7 @@ export const apiClient = {
|
|||
destroyReaction: async (reaction_id: number) => {
|
||||
const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise()
|
||||
console.debug('[graphql.client.core] destroyReaction:', response)
|
||||
return response.data.delete_reaction.reaction
|
||||
return response.data.delete_reaction
|
||||
},
|
||||
updateReaction: async (reaction: ReactionInput) => {
|
||||
const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise()
|
||||
|
@ -200,15 +199,18 @@ export const apiClient = {
|
|||
console.debug('[graphql.client.core] authorsLoadBy:', resp)
|
||||
return resp.data.load_authors_by
|
||||
},
|
||||
|
||||
getShoutBySlug: async (slug: string) => {
|
||||
const resp = await publicGraphQLClient.query(shoutLoad, { slug }).toPromise()
|
||||
return resp.data.get_shout
|
||||
},
|
||||
getShoutById: async (shout_id: number) => {
|
||||
const resp = await publicGraphQLClient.query(shoutLoad, { shout_id }).toPromise()
|
||||
|
||||
getMyShout: async (shout_id: number) => {
|
||||
await apiClient.private
|
||||
const resp = await apiClient.private.query(getMyShout, { shout_id }).toPromise()
|
||||
if (resp.error) console.error(resp)
|
||||
|
||||
return resp.data.get_shout
|
||||
return resp.data.get_my_shout
|
||||
},
|
||||
|
||||
getShouts: async (options: LoadShoutsOptions) => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query LoadShoutQuery($slug: String, $shout_id: Int) {
|
||||
get_shout(slug: $slug, shout_id: $shout_id) {
|
||||
query LoadShoutQuery($slug: String!) {
|
||||
get_shout(slug: $slug) {
|
||||
id
|
||||
title
|
||||
lead
|
||||
|
|
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`
|
||||
query LoadDraftsQuery {
|
||||
load_shouts_drafts {
|
||||
get_shouts_drafts {
|
||||
id
|
||||
title
|
||||
subtitle
|
||||
|
@ -35,7 +35,6 @@ export default gql`
|
|||
featured_at
|
||||
stat {
|
||||
viewed
|
||||
|
||||
rating
|
||||
commented
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export default gql`
|
|||
shouts
|
||||
authors
|
||||
followers
|
||||
comments
|
||||
# viewed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,22 +7,42 @@ import { useLocalize } from '../context/localize'
|
|||
import { apiClient } from '../graphql/client/core'
|
||||
import { Shout } from '../graphql/schema/core.gen'
|
||||
import { useRouter } from '../stores/router'
|
||||
import { router } from '../stores/router'
|
||||
|
||||
import { redirectPage } from '@nanostores/router'
|
||||
import { useSnackbar } from '../context/snackbar'
|
||||
import { LayoutType } from './types'
|
||||
|
||||
const EditView = lazy(() => import('../components/Views/EditView/EditView'))
|
||||
|
||||
export const EditPage = () => {
|
||||
const { page } = useRouter()
|
||||
const snackbar = useSnackbar()
|
||||
const { t } = useLocalize()
|
||||
|
||||
const shoutId = createMemo(() => Number((page().params as Record<'shoutId', string>).shoutId))
|
||||
|
||||
const [shout, setShout] = createSignal<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 () => {
|
||||
const loadedShout = await apiClient.getShoutById(shoutId())
|
||||
setShout(loadedShout)
|
||||
const shout_id = window.location.pathname.split('/').pop()
|
||||
if (shout_id) {
|
||||
try {
|
||||
await loadMyShout(parseInt(shout_id, 10))
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const title = createMemo(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@ export const byCreated = (a: Shout | Reaction, b: Shout | Reaction) => {
|
|||
}
|
||||
|
||||
export const byPublished = (a: Shout, b: Shout) => {
|
||||
return a.published_at - b.published_at
|
||||
return (a?.published_at || 0) - (b?.published_at || 0)
|
||||
}
|
||||
|
||||
export const byLength = (
|
||||
|
|
Loading…
Reference in New Issue
Block a user