Merge branch 'feature/empty-feed' of github.com:Discours/discoursio-webapp into feature/empty-feed
This commit is contained in:
commit
5f194c7a3b
|
@ -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",
|
||||||
|
|
|
@ -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": "Скопировать ссылку",
|
||||||
|
@ -297,9 +298,9 @@
|
||||||
"Paste Embed code": "Вставьте embed код",
|
"Paste Embed code": "Вставьте embed код",
|
||||||
"Personal": "Личные",
|
"Personal": "Личные",
|
||||||
"Pin": "Закрепить",
|
"Pin": "Закрепить",
|
||||||
"Placeholder feed": "Подпишитесь на любимые темы, авторов и сообщества — моментально узнавайте о новых публикациях и обсуждениях",
|
"Placeholder feed": "Подпишитесь на любимые темы, авторов и сообщества — <br/>моментально узнавайте о новых публикациях и обсуждениях",
|
||||||
"Placeholder feedCollaborations": "На платформе можно писать материалы вместе. Здесь появятся публикации, в которые вы внесли вклад",
|
"Placeholder feedCollaborations": "На платформе можно писать материалы вместе. <br/>Здесь появятся публикации, в которые вы внесли вклад",
|
||||||
"Placeholder feedDiscussions": "Дискурс — свободная платформа для осмысленного общения. Здесь появятся все ваши реплики, чтобы в любой момент вернуться к диалогу",
|
"Placeholder feedDiscussions": "Дискурс — свободная платформа для осмысленного общения. <br/>Здесь появятся все ваши реплики, чтобы в любой момент вернуться к диалогу",
|
||||||
"Platform Guide": "Гид по дискурсу",
|
"Platform Guide": "Гид по дискурсу",
|
||||||
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
|
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
|
||||||
"Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля",
|
"Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля",
|
||||||
|
@ -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": "просмотр",
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
.placeholder {
|
.placeholder {
|
||||||
border-radius: 2.2rem;
|
border-radius: 2.2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@include font-size(1.4rem);
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@include font-size(2.4rem);
|
font-size: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
@ -17,6 +17,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
@include font-size(1.5rem);
|
@include font-size(1.5rem);
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
|
justify-content: center;
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -29,49 +30,72 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder--feed-mode {
|
.placeholder--feed-mode {
|
||||||
aspect-ratio: 1 / 0.8;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 40rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:after {
|
@include media-breakpoint-up(lg) {
|
||||||
bottom: 0;
|
aspect-ratio: 1 / 0.8;
|
||||||
content: '';
|
|
||||||
height: 20%;
|
|
||||||
left: 0;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.placeholder--feed & {
|
|
||||||
background: linear-gradient(to top, #171032, rgba(23, 16, 50, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder--feedCollaborations & {
|
|
||||||
background: linear-gradient(to top, #070709, rgba(7, 7, 9, 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholderCover {
|
.placeholderCover {
|
||||||
flex: 0 100%;
|
flex: 1 100%;
|
||||||
width: 100%;
|
position: relative;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
bottom: 0;
|
||||||
|
content: '';
|
||||||
|
height: 20%;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.placeholder--feedMy .placeholderCover:after {
|
||||||
|
background: linear-gradient(to top, #171032, rgba(23, 16, 50, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.placeholder--feedCollaborations .placeholderCover:after {
|
||||||
|
background: linear-gradient(to top, #070709, rgba(7, 7, 9, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder--profile-mode {
|
.placeholder--profile-mode {
|
||||||
min-height: 28rem;
|
min-height: 40rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(lg) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
max-height: 30rem;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.placeholderCover {
|
.placeholderCover {
|
||||||
flex: 0 45rem;
|
|
||||||
min-width: 45rem;
|
|
||||||
order: 2;
|
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
//flex: 0 50%;
|
||||||
|
//min-width: 50%;
|
||||||
|
order: 2;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: auto;
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
//width: auto;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
object-position: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,9 +103,19 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@include font-size(2rem);
|
font-size: 1.4rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
padding: 3rem;
|
min-width: 60%;
|
||||||
|
padding: 0 2rem 2rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
@ -90,11 +124,18 @@
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
background: var(--background-color-invert);
|
background: var(--background-color-invert);
|
||||||
color: var(--default-color-invert);
|
|
||||||
bottom: 2rem;
|
bottom: 2rem;
|
||||||
position: absolute;
|
color: var(--default-color-invert);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
left: 2rem;
|
||||||
right: 2rem;
|
right: 2rem;
|
||||||
width: auto;
|
width: 100%;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
left: auto;
|
||||||
|
position: absolute;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
|
@ -115,9 +156,15 @@
|
||||||
|
|
||||||
.placeholderContent {
|
.placeholderContent {
|
||||||
padding: 1.6rem;
|
padding: 1.6rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(lg) {
|
||||||
|
br {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder--feed,
|
.placeholder--feedMy,
|
||||||
.placeholder--feedCollaborations {
|
.placeholder--feedCollaborations {
|
||||||
color: var(--default-color-invert);
|
color: var(--default-color-invert);
|
||||||
|
|
||||||
|
@ -128,7 +175,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder--feed {
|
.placeholder--feedMy {
|
||||||
background: #171032;
|
background: #171032;
|
||||||
|
|
||||||
.placeholderCover {
|
.placeholderCover {
|
||||||
|
@ -190,6 +237,11 @@
|
||||||
@include font-size(1.6rem);
|
@include font-size(1.6rem);
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-down(sm) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
padding-left: 2.6rem;
|
padding-left: 2.6rem;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const Placeholder = (props: PlaceholderProps) => {
|
||||||
const { author } = useSession()
|
const { author } = useSession()
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
feed: {
|
feedMy: {
|
||||||
image: 'placeholder-feed.webp',
|
image: 'placeholder-feed.webp',
|
||||||
header: t('Feed settings'),
|
header: t('Feed settings'),
|
||||||
text: t('Placeholder feed'),
|
text: t('Placeholder feed'),
|
||||||
|
|
|
@ -95,33 +95,37 @@ export const LoginForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { errors } = await signIn({ email: email(), password: password() })
|
const { errors } = await signIn({ email: email(), password: password() })
|
||||||
console.error('[signIn errors]', errors)
|
|
||||||
if (errors?.length > 0) {
|
if (errors?.length > 0) {
|
||||||
if (
|
console.warn('[signIn] errors: ', errors)
|
||||||
errors.some(
|
errors.forEach((error) => {
|
||||||
(error) =>
|
switch (error.message) {
|
||||||
error.message.includes('bad user credentials') || error.message.includes('user not found'),
|
case 'user has not signed up email & password': {
|
||||||
)
|
setValidationErrors((prev) => ({
|
||||||
) {
|
...prev,
|
||||||
setValidationErrors((prev) => ({
|
password: t('Something went wrong, check email and password'),
|
||||||
...prev,
|
}))
|
||||||
password: t('Something went wrong, check email and password'),
|
break
|
||||||
}))
|
}
|
||||||
} else if (errors.some((error) => error.message.includes('user not found'))) {
|
case 'user not found': {
|
||||||
setSubmitError('Пользователь не найден')
|
setValidationErrors((prev) => ({ ...prev, email: t('User was not found') }))
|
||||||
} else if (errors.some((error) => error.message.includes('email not verified'))) {
|
break
|
||||||
setSubmitError(
|
}
|
||||||
<div class={styles.info}>
|
case 'email not verified': {
|
||||||
{t('This email is not verified')}
|
setValidationErrors((prev) => ({ ...prev, email: t('This email is not verified') }))
|
||||||
{'. '}
|
break
|
||||||
<span class={'link'} onClick={handleSendLinkAgainClick}>
|
}
|
||||||
{t('Send link again')}
|
default:
|
||||||
</span>
|
setSubmitError(
|
||||||
</div>,
|
<div class={styles.info}>
|
||||||
)
|
{t('Error', errors[0].message)}
|
||||||
} else {
|
{'. '}
|
||||||
setSubmitError(t('Error', errors[0].message))
|
<span class={'link'} onClick={handleSendLinkAgainClick}>
|
||||||
}
|
{t('Send link again')}
|
||||||
|
</span>
|
||||||
|
</div>,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
hideModal()
|
hideModal()
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()}>
|
||||||
|
|
|
@ -260,28 +260,32 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={getPage().route === 'authorComments'}>
|
<Match when={getPage().route === 'authorComments'}>
|
||||||
<div class="wide-container">
|
<Show when={session()?.user?.app_data?.profile?.slug === props.authorSlug && !commented().length}>
|
||||||
<Placeholder type={getPage().route} mode="profile" />
|
<div class="wide-container">
|
||||||
</div>
|
<Placeholder type={getPage().route} mode="profile" />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div class="wide-container">
|
<Show when={commented()}>
|
||||||
<div class="row">
|
<div class="wide-container">
|
||||||
<div class="col-md-20 col-lg-18">
|
<div class="row">
|
||||||
<ul class={stylesArticle.comments}>
|
<div class="col-md-20 col-lg-18">
|
||||||
<For each={commented()?.sort(byCreated).reverse()}>
|
<ul class={stylesArticle.comments}>
|
||||||
{(comment) => (
|
<For each={commented()?.sort(byCreated).reverse()}>
|
||||||
<Comment
|
{(comment) => (
|
||||||
comment={comment}
|
<Comment
|
||||||
class={styles.comment}
|
comment={comment}
|
||||||
showArticleLink={true}
|
class={styles.comment}
|
||||||
onDelete={(id) => handleDeleteComment(id)}
|
showArticleLink={true}
|
||||||
/>
|
onDelete={(id) => handleDeleteComment(id)}
|
||||||
)}
|
/>
|
||||||
</For>
|
)}
|
||||||
</ul>
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={getPage().route === 'author'}>
|
<Match when={getPage().route === 'author'}>
|
||||||
<Show
|
<Show
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -235,10 +235,11 @@ export const FeedView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-12 offset-xl-1">
|
<div class="col-md-12 offset-xl-1">
|
||||||
<Show
|
<Show when={!author() && page().route !== 'feed'}>
|
||||||
when={author() || !sortedArticles().length}
|
<Placeholder type={page().route} mode="feed" />
|
||||||
fallback={<Placeholder type={page().route} mode="feed" />}
|
</Show>
|
||||||
>
|
|
||||||
|
<Show when={(author() || page().route === 'feed') && sortedArticles().length}>
|
||||||
<div class={styles.filtersContainer}>
|
<div class={styles.filtersContainer}>
|
||||||
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
||||||
<li
|
<li
|
||||||
|
|
|
@ -30,53 +30,57 @@ const ConnectContext = createContext<ConnectContextType>()
|
||||||
|
|
||||||
export const ConnectProvider = (props: { children: JSX.Element }) => {
|
export const ConnectProvider = (props: { children: JSX.Element }) => {
|
||||||
const [messageHandlers, setHandlers] = createSignal<MessageHandler[]>([])
|
const [messageHandlers, setHandlers] = createSignal<MessageHandler[]>([])
|
||||||
// const [messages, setMessages] = createSignal<Array<SSEMessage>>([]);
|
|
||||||
const [connected, setConnected] = createSignal(false)
|
const [connected, setConnected] = createSignal(false)
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
const [retried, setRetried] = createSignal<number>(0)
|
||||||
|
|
||||||
const addHandler = (handler: MessageHandler) => {
|
const addHandler = (handler: MessageHandler) => {
|
||||||
setHandlers((hhh) => [...hhh, handler])
|
setHandlers((hhh) => [...hhh, handler])
|
||||||
}
|
}
|
||||||
|
|
||||||
const [retried, setRetried] = createSignal<number>(0)
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
const token = session()?.access_token
|
const token = session()?.access_token
|
||||||
if (token && !connected()) {
|
if (token && !connected() && retried() <= RECONNECT_TIMES) {
|
||||||
console.info('[context.connect] init SSE connection')
|
console.info('[context.connect] init SSE connection')
|
||||||
await fetchEventSource('https://connect.discours.io', {
|
try {
|
||||||
method: 'GET',
|
await fetchEventSource('https://connect.discours.io', {
|
||||||
headers: {
|
method: 'GET',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
Authorization: token,
|
'Content-Type': 'application/json',
|
||||||
},
|
Authorization: token,
|
||||||
onmessage(event) {
|
},
|
||||||
const m: SSEMessage = JSON.parse(event.data || '{}')
|
onmessage(event) {
|
||||||
console.log('[context.connect] Received message:', m)
|
const m: SSEMessage = JSON.parse(event.data || '{}')
|
||||||
|
console.log('[context.connect] Received message:', m)
|
||||||
// Iterate over all registered handlers and call them
|
messageHandlers().forEach((handler) => handler(m))
|
||||||
messageHandlers().forEach((handler) => handler(m))
|
},
|
||||||
},
|
onopen: (response) => {
|
||||||
async onopen(response) {
|
console.log('[context.connect] SSE connection opened', response)
|
||||||
console.log('[context.connect] SSE connection opened', response)
|
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
||||||
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
setConnected(true)
|
||||||
setConnected(true)
|
setRetried(0)
|
||||||
} else if (response.status === 401) {
|
return Promise.resolve()
|
||||||
throw new Error('SSE: cannot connect to real-time updates')
|
}
|
||||||
} else {
|
return Promise.reject(`SSE: cannot connect to real-time updates, status: ${response.status}`)
|
||||||
setRetried((r) => r + 1)
|
},
|
||||||
throw new Error(`SSE: failed to connect ${retried()} times`)
|
onclose() {
|
||||||
}
|
console.log('[context.connect] SSE connection closed by server')
|
||||||
},
|
setConnected(false)
|
||||||
onclose() {
|
if (retried() < RECONNECT_TIMES) {
|
||||||
console.log('[context.connect] SSE connection closed by server')
|
setRetried((r) => r + 1)
|
||||||
setConnected(false)
|
}
|
||||||
},
|
},
|
||||||
onerror(err) {
|
onerror(err) {
|
||||||
if (err.message === 'unauthorized' || retried() > RECONNECT_TIMES) {
|
console.error('[context.connect] SSE connection error:', err)
|
||||||
throw err // rethrow to stop the operation
|
setConnected(false)
|
||||||
}
|
if (retried() < RECONNECT_TIMES) {
|
||||||
},
|
setRetried((r) => r + 1)
|
||||||
})
|
} else throw Error(err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[context.connect] SSE connection failed:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js'
|
import { Show, Suspense, createEffect, createMemo, createSignal, lazy, on } from 'solid-js'
|
||||||
|
|
||||||
import { AuthGuard } from '../components/AuthGuard'
|
import { AuthGuard } from '../components/AuthGuard'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
|
@ -7,7 +7,7 @@ import { useLocalize } from '../context/localize'
|
||||||
import { useSession } from '../context/session'
|
import { useSession } from '../context/session'
|
||||||
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 { router } from '../stores/router'
|
import { router, useRouter } from '../stores/router'
|
||||||
|
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { useSnackbar } from '../context/snackbar'
|
import { useSnackbar } from '../context/snackbar'
|
||||||
|
@ -33,6 +33,7 @@ const getContentTypeTitle = (layout: LayoutType) => {
|
||||||
export const EditPage = () => {
|
export const EditPage = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
const { page } = useRouter()
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
|
|
||||||
const fail = async (error: string) => {
|
const fail = async (error: string) => {
|
||||||
|
@ -45,12 +46,22 @@ export const EditPage = () => {
|
||||||
const [shoutId, setShoutId] = createSignal<number>(0)
|
const [shoutId, setShoutId] = createSignal<number>(0)
|
||||||
const [shout, setShout] = createSignal<Shout>()
|
const [shout, setShout] = createSignal<Shout>()
|
||||||
|
|
||||||
onMount(() => {
|
createEffect(
|
||||||
const shoutId = window.location.pathname.split('/').pop()
|
on(
|
||||||
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
() => page(),
|
||||||
console.debug(`editing shout ${shoutIdFromUrl}`)
|
(p) => {
|
||||||
if (shoutIdFromUrl) setShoutId(shoutIdFromUrl)
|
if (p?.path) {
|
||||||
})
|
console.debug(p?.path)
|
||||||
|
const shoutId = p?.path.split('/').pop()
|
||||||
|
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||||
|
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||||
|
if (shoutIdFromUrl) {
|
||||||
|
setShoutId(shoutIdFromUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on([session, shout, shoutId], async ([ses, sh, shid]) => {
|
on([session, shout, shoutId], async ([ses, sh, shid]) => {
|
||||||
|
@ -63,6 +74,7 @@ export const EditPage = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
{ defer: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
const title = createMemo(() => {
|
const title = createMemo(() => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Match, Switch, createEffect, on, onCleanup } from 'solid-js'
|
import { Match, Switch, createEffect, on, onCleanup } from 'solid-js'
|
||||||
|
|
||||||
import { AuthGuard } from '../components/AuthGuard'
|
|
||||||
import { Feed } from '../components/Views/Feed'
|
import { Feed } from '../components/Views/Feed'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
|
@ -48,9 +47,7 @@ export const FeedPage = () => {
|
||||||
<Feed loadShouts={handleFeedLoadShouts} />
|
<Feed loadShouts={handleFeedLoadShouts} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={page().route === 'feedMy'}>
|
<Match when={page().route === 'feedMy'}>
|
||||||
<AuthGuard>
|
<Feed loadShouts={handleMyFeedLoadShouts} />
|
||||||
<Feed loadShouts={handleMyFeedLoadShouts} />
|
|
||||||
</AuthGuard>
|
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ReactionsProvider>
|
</ReactionsProvider>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user