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/
/playwright/.cache/
/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 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"
}

View File

@ -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": "Восстановить пароль"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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`
query LoadDraftsQuery {
load_shouts_drafts {
get_shouts_drafts {
id
title
subtitle
@ -35,7 +35,6 @@ export default gql`
featured_at
stat {
viewed
rating
commented
}

View File

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

View File

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

View File

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