Merge branch 'dev' of https://github.com/Discours/discoursio-webapp into feature/empty-feed

This commit is contained in:
kvakazyambra 2024-05-20 23:41:56 +03:00
commit 417c57e338
7 changed files with 78 additions and 68 deletions

View File

@ -93,6 +93,7 @@
"Community Principles": "Community Principles", "Community Principles": "Community Principles",
"Community values and rules of engagement for the open editorial team": "Community values and rules of engagement for the open editorial team", "Community values and rules of engagement for the open editorial team": "Community values and rules of engagement for the open editorial team",
"Confirm": "Confirm", "Confirm": "Confirm",
"Contents": "Contents",
"Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom", "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom",
"Cooperate": "Cooperate", "Cooperate": "Cooperate",
"Copy link": "Copy link", "Copy link": "Copy link",
@ -432,6 +433,7 @@
"Username": "Username", "Username": "Username",
"Userpic": "Userpic", "Userpic": "Userpic",
"Users": "Users", "Users": "Users",
"User was not found": "User was not found",
"Video format not supported": "Video format not supported", "Video format not supported": "Video format not supported",
"Video": "Video", "Video": "Video",
"Views": "Views", "Views": "Views",
@ -484,7 +486,6 @@
"cancel": "cancel", "cancel": "cancel",
"collections": "collections", "collections": "collections",
"community": "community", "community": "community",
"contents": "contents",
"delimiter": "delimiter", "delimiter": "delimiter",
"discussion": "Discours", "discussion": "Discours",
"dogma keywords": "Discours.io, dogma, editorial principles, code of ethics, journalism, community", "dogma keywords": "Discours.io, dogma, editorial principles, code of ethics, journalism, community",

View File

@ -97,6 +97,7 @@
"Community Principles": "Принципы сообщества", "Community Principles": "Принципы сообщества",
"Community values and rules of engagement for the open editorial team": "Ценности сообщества и правила взаимодействия открытой редакции", "Community values and rules of engagement for the open editorial team": "Ценности сообщества и правила взаимодействия открытой редакции",
"Confirm": "Подтвердить", "Confirm": "Подтвердить",
"Contents": "Оглавление",
"Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Внесите вклад в свободный самиздат. Поддержите Дискурс — независимое некоммерческое издание, которое работает только для вас. Станьте опорой открытой редакции", "Contribute to free samizdat. Support Discours - an independent non-profit publication that works only for you. Become a pillar of the open newsroom": "Внесите вклад в свободный самиздат. Поддержите Дискурс — независимое некоммерческое издание, которое работает только для вас. Станьте опорой открытой редакции",
"Cooperate": "Соучаствовать", "Cooperate": "Соучаствовать",
"Copy link": "Скопировать ссылку", "Copy link": "Скопировать ссылку",
@ -507,7 +508,6 @@
"cancel": "отменить", "cancel": "отменить",
"collections": "коллекции", "collections": "коллекции",
"community": "сообщество", "community": "сообщество",
"contents": "оглавление",
"create_chat": "Создать чат", "create_chat": "Создать чат",
"create_group": "Создать группу", "create_group": "Создать группу",
"delimiter": "разделитель", "delimiter": "разделитель",
@ -564,6 +564,7 @@
"topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования", "topicKeywords": "{topic}, Discours.io, статьи, журналистика, исследования",
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"User was not found": "Пользователь не найден",
"verified": "уже подтверждён", "verified": "уже подтверждён",
"video": "видео", "video": "видео",
"view": "просмотр", "view": "просмотр",

View File

@ -96,32 +96,36 @@ export const LoginForm = () => {
try { try {
const { errors } = await signIn({ email: email(), password: password() }) const { errors } = await signIn({ email: email(), password: password() })
if (errors?.length > 0) { if (errors?.length > 0) {
console.debug('[signIn] errors:', errors) console.warn('[signIn] errors: ', errors)
if ( errors.forEach((error) => {
errors.some( switch (error.message) {
(error) => case 'user has not signed up email & password': {
error.message.includes('bad user credentials') || error.message.includes('user not found'), setValidationErrors((prev) => ({
) ...prev,
) { password: t('Something went wrong, check email and password'),
setValidationErrors((prev) => ({ }))
...prev, break
password: t('Something went wrong, check email and password'), }
})) case 'user not found': {
} else if (errors.some((error) => error.message.includes('user not found'))) { setValidationErrors((prev) => ({ ...prev, email: t('User was not found') }))
setSubmitError('Пользователь не найден') break
} else if (errors.some((error) => error.message.includes('email not verified'))) { }
setSubmitError( case 'email not verified': {
<div class={styles.info}> setValidationErrors((prev) => ({ ...prev, email: t('This email is not verified') }))
{t('This email is not verified')} break
{'. '} }
<span class={'link'} onClick={handleSendLinkAgainClick}> default:
{t('Send link again')} setSubmitError(
</span> <div class={styles.info}>
</div>, {t('Error', errors[0].message)}
) {'. '}
} else { <span class={'link'} onClick={handleSendLinkAgainClick}>
setSubmitError(t('Error', errors[0].message)) {t('Send link again')}
} </span>
</div>,
)
}
})
return return
} }
hideModal() hideModal()

View File

@ -157,7 +157,7 @@
color: #000; color: #000;
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 500;
line-height: 1.8rem; line-height: 1.8rem;
text-align: left; text-align: left;
vertical-align: bottom; vertical-align: bottom;

View File

@ -86,7 +86,7 @@ export const TableOfContents = (props: Props) => {
<Show when={isVisible()}> <Show when={isVisible()}>
<div class={styles.TableOfContentsContainerInner}> <div class={styles.TableOfContentsContainerInner}>
<div class={styles.TableOfContentsHeader}> <div class={styles.TableOfContentsHeader}>
<p class={styles.TableOfContentsHeading}>{t('contents')}</p> <p class={styles.TableOfContentsHeading}>{t('Contents')}</p>
</div> </div>
<ul class={styles.TableOfContentsHeadingsList}> <ul class={styles.TableOfContentsHeadingsList}>
<For each={headings()}> <For each={headings()}>

View File

@ -2,7 +2,7 @@ import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { throttle } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -42,9 +42,8 @@ export const EMPTY_TOPIC: Topic = {
slug: '', slug: '',
} }
const THROTTLING_INTERVAL = 2000 const AUTO_SAVE_DELAY = 3000
const AUTO_SAVE_INTERVAL = 5000
const AUTO_SAVE_DELAY = 5000
const handleScrollTopButtonClick = (e) => { const handleScrollTopButtonClick = (e) => {
e.preventDefault() e.preventDefault()
window.scrollTo({ window.scrollTo({
@ -104,6 +103,8 @@ export const EditView = (props: Props) => {
return JSON.parse(form.media || '[]') return JSON.parse(form.media || '[]')
}) })
const [hasChanges, setHasChanges] = createSignal(false)
onMount(() => { onMount(() => {
const handleScroll = () => { const handleScroll = () => {
setIsScrolled(window.scrollY > 0) setIsScrolled(window.scrollY > 0)
@ -113,7 +114,7 @@ export const EditView = (props: Props) => {
onCleanup(() => { onCleanup(() => {
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
}) })
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleBeforeUnload = (event) => { const handleBeforeUnload = (event) => {
if (!deepEqual(prevForm, form)) { if (!deepEqual(prevForm, form)) {
event.returnValue = t( event.returnValue = t(
@ -127,8 +128,8 @@ export const EditView = (props: Props) => {
}) })
const handleTitleInputChange = (value: string) => { const handleTitleInputChange = (value: string) => {
setForm('title', value) handleInputChange('title', value)
setForm('slug', slugify(value)) handleInputChange('slug', slugify(value))
if (value) { if (value) {
setFormErrors('title', '') setFormErrors('title', '')
} }
@ -136,21 +137,21 @@ export const EditView = (props: Props) => {
const handleAddMedia = (data) => { const handleAddMedia = (data) => {
const newMedia = [...mediaItems(), ...data] const newMedia = [...mediaItems(), ...data]
setForm('media', JSON.stringify(newMedia)) handleInputChange('media', JSON.stringify(newMedia))
} }
const handleSortedMedia = (data) => { const handleSortedMedia = (data) => {
setForm('media', JSON.stringify(data)) handleInputChange('media', JSON.stringify(data))
} }
const handleMediaDelete = (index) => { const handleMediaDelete = (index) => {
const copy = [...mediaItems()] const copy = [...mediaItems()]
copy.splice(index, 1) copy.splice(index, 1)
setForm('media', JSON.stringify(copy)) handleInputChange('media', JSON.stringify(copy))
} }
const handleMediaChange = (index, value) => { const handleMediaChange = (index, value) => {
const updated = mediaItems().map((item, idx) => (idx === index ? value : item)) const updated = mediaItems().map((item, idx) => (idx === index ? value : item))
setForm('media', JSON.stringify(updated)) handleInputChange('media', JSON.stringify(updated))
} }
const [baseAudioFields, setBaseAudioFields] = createSignal({ const [baseAudioFields, setBaseAudioFields] = createSignal({
@ -162,7 +163,7 @@ export const EditView = (props: Props) => {
const handleBaseFieldsChange = (key, value) => { const handleBaseFieldsChange = (key, value) => {
if (mediaItems().length > 0) { if (mediaItems().length > 0) {
const updated = mediaItems().map((media) => ({ ...media, [key]: value })) const updated = mediaItems().map((media) => ({ ...media, [key]: value }))
setForm('media', JSON.stringify(updated)) handleInputChange('media', JSON.stringify(updated))
} else { } else {
setBaseAudioFields({ ...baseAudioFields(), [key]: value }) setBaseAudioFields({ ...baseAudioFields(), [key]: value })
} }
@ -182,34 +183,32 @@ export const EditView = (props: Props) => {
} }
} }
let autoSaveTimeOutId: number | string | NodeJS.Timeout
const autoSave = async () => { const autoSave = async () => {
const hasChanges = !deepEqual(form, prevForm) console.log('autoSave called')
const hasTopic = Boolean(form.mainTopic) if (hasChanges()) {
if (hasChanges || hasTopic) {
console.debug('saving draft', form) console.debug('saving draft', form)
setSaving(true) setSaving(true)
saveDraftToLocalStorage(form) saveDraftToLocalStorage(form)
await saveDraft(form) await saveDraft(form)
setPrevForm(clone(form)) setPrevForm(clone(form))
setTimeout(() => setSaving(false), AUTO_SAVE_DELAY) setSaving(false)
setHasChanges(false)
} }
} }
// Throttle the autoSave function const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, autoSave)
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
const autoSaveRecursive = () => { const handleInputChange = (key, value) => {
autoSaveTimeOutId = setTimeout(() => { console.log(`[handleInputChange] ${key}: ${value}`)
throttledAutoSave() setForm(key, value)
autoSaveRecursive() setHasChanges(true)
}, AUTO_SAVE_INTERVAL) debouncedAutoSave()
} }
onMount(() => { onMount(() => {
autoSaveRecursive() onCleanup(() => {
onCleanup(() => clearTimeout(autoSaveTimeOutId)) debouncedAutoSave.cancel()
})
}) })
const showSubtitleInput = () => { const showSubtitleInput = () => {
@ -310,7 +309,7 @@ export const EditView = (props: Props) => {
subtitleInput.current = el subtitleInput.current = el
}} }}
allowEnterKey={false} allowEnterKey={false}
value={(value) => setForm('subtitle', value || '')} value={(value) => handleInputChange('subtitle', value || '')}
class={styles.subtitleInput} class={styles.subtitleInput}
placeholder={t('Subheader')} placeholder={t('Subheader')}
initialValue={form.subtitle || ''} initialValue={form.subtitle || ''}
@ -324,7 +323,7 @@ export const EditView = (props: Props) => {
smallHeight={true} smallHeight={true}
placeholder={t('A short introduction to keep the reader interested')} placeholder={t('A short introduction to keep the reader interested')}
initialContent={form.lead} initialContent={form.lead}
onChange={(value) => setForm('lead', value)} onChange={(value) => handleInputChange('lead', value)}
/> />
</Show> </Show>
</Show> </Show>
@ -345,7 +344,7 @@ export const EditView = (props: Props) => {
} }
isMultiply={false} isMultiply={false}
fileType={'image'} fileType={'image'}
onUpload={(val) => setForm('coverImageUrl', val[0].url)} onUpload={(val) => handleInputChange('coverImageUrl', val[0].url)}
/> />
} }
> >
@ -362,7 +361,7 @@ export const EditView = (props: Props) => {
<div <div
ref={triggerRef} ref={triggerRef}
class={styles.delete} class={styles.delete}
onClick={() => setForm('coverImageUrl', null)} onClick={() => handleInputChange('coverImageUrl', null)}
> >
<Icon name="close-white" /> <Icon name="close-white" />
</div> </div>
@ -408,7 +407,7 @@ export const EditView = (props: Props) => {
<Editor <Editor
shoutId={form.shoutId} shoutId={form.shoutId}
initialContent={form.body} initialContent={form.body}
onChange={(body) => setForm('body', body)} onChange={(body) => handleInputChange('body', body)}
/> />
</Show> </Show>
</div> </div>

View File

@ -48,14 +48,18 @@ export const EditPage = () => {
createEffect( createEffect(
on( on(
page, () => page(),
(p) => { (p) => {
const shoutId = p?.path.split('/').pop() if (p?.path) {
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10) console.debug(p?.path)
console.debug(`editing shout ${shoutIdFromUrl}`) const shoutId = p?.path.split('/').pop()
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl) const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
console.debug(`editing shout ${shoutIdFromUrl}`)
if (shoutIdFromUrl) {
setShoutId(shoutIdFromUrl)
}
}
}, },
{ defer: true },
), ),
) )
@ -70,6 +74,7 @@ export const EditPage = () => {
} }
} }
}), }),
{ defer: true },
) )
const title = createMemo(() => { const title = createMemo(() => {