merged
This commit is contained in:
commit
26ba530f9e
|
@ -211,6 +211,7 @@
|
||||||
"New stories every day and even more!": "New stories and more are waiting for you every day!",
|
"New stories every day and even more!": "New stories and more are waiting for you every day!",
|
||||||
"Newsletter": "Newsletter",
|
"Newsletter": "Newsletter",
|
||||||
"Night mode": "Night mode",
|
"Night mode": "Night mode",
|
||||||
|
"No notifications, yet": "No notifications, yet",
|
||||||
"No such account, please try to register": "No such account found, please try to register",
|
"No such account, please try to register": "No such account found, please try to register",
|
||||||
"Nothing here yet": "There's nothing here yet",
|
"Nothing here yet": "There's nothing here yet",
|
||||||
"Nothing is here": "There is nothing here",
|
"Nothing is here": "There is nothing here",
|
||||||
|
@ -244,6 +245,7 @@
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Profile settings": "Profile settings",
|
"Profile settings": "Profile settings",
|
||||||
"Publications": "Publications",
|
"Publications": "Publications",
|
||||||
|
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}",
|
||||||
"Publish Album": "Publish Album",
|
"Publish Album": "Publish Album",
|
||||||
"Publish Settings": "Publish Settings",
|
"Publish Settings": "Publish Settings",
|
||||||
"Punchline": "Punchline",
|
"Punchline": "Punchline",
|
||||||
|
@ -413,5 +415,6 @@
|
||||||
"video": "video",
|
"video": "video",
|
||||||
"view": "view",
|
"view": "view",
|
||||||
"zine": "zine",
|
"zine": "zine",
|
||||||
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}"
|
"SubscriptionWithPlurals": "{count, plural, =0 {no subscriptions} one {{count} subscription} other {{count} subscriptions}",
|
||||||
|
"Edit profile": "Edit profile"
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,6 +221,7 @@
|
||||||
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
|
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
|
||||||
"Newsletter": "Рассылка",
|
"Newsletter": "Рассылка",
|
||||||
"Night mode": "Ночная тема",
|
"Night mode": "Ночная тема",
|
||||||
|
"No notifications, yet": "Тут пока пусто",
|
||||||
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
|
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
|
||||||
"Nothing here yet": "Здесь пока ничего нет",
|
"Nothing here yet": "Здесь пока ничего нет",
|
||||||
"Nothing is here": "Здесь ничего нет",
|
"Nothing is here": "Здесь ничего нет",
|
||||||
|
@ -257,6 +258,7 @@
|
||||||
"Profile successfully saved": "Профиль успешно сохранён",
|
"Profile successfully saved": "Профиль успешно сохранён",
|
||||||
"Publication settings": "Настройки публикации",
|
"Publication settings": "Настройки публикации",
|
||||||
"Publications": "Публикации",
|
"Publications": "Публикации",
|
||||||
|
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}",
|
||||||
"Publish": "Опубликовать",
|
"Publish": "Опубликовать",
|
||||||
"Publish Album": "Опубликовать альбом",
|
"Publish Album": "Опубликовать альбом",
|
||||||
"Publish Settings": "Настройки публикации",
|
"Publish Settings": "Настройки публикации",
|
||||||
|
@ -430,14 +432,15 @@
|
||||||
"subscriber": "подписчик",
|
"subscriber": "подписчик",
|
||||||
"subscriber_rp": "подписчика",
|
"subscriber_rp": "подписчика",
|
||||||
"subscribers": "подписчиков",
|
"subscribers": "подписчиков",
|
||||||
"subscription": "подписка",
|
|
||||||
"subscription_rp": "подписки",
|
|
||||||
"subscriptions": "подписок",
|
|
||||||
"terms of use": "правилами пользования сайтом",
|
"terms of use": "правилами пользования сайтом",
|
||||||
"topics": "темы",
|
"topics": "темы",
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
"video": "видео",
|
"video": "видео",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал",
|
"zine": "журнал",
|
||||||
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}"
|
"SubscriberWithCount": "{count, plural, =0 {нет подписчиков} one {{count} подписчик} few {{count} подписчика} other {{count} подписчиков}}",
|
||||||
|
"SubscriptionWithCount": "{count, plural, =0 {нет подписок} one {{count} подписка} few {{count} подписки} other {{count} подписок}}",
|
||||||
|
"Edit profile": "Редактировать профиль",
|
||||||
|
"NewCommentNotificationText": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
|
||||||
|
"NewReplyNotificationText": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} к вашему комментарию к публикации {shoutTitle} от {lastCommenterName}{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { SnackbarProvider } from '../context/snackbar'
|
||||||
import { LocalizeProvider } from '../context/localize'
|
import { LocalizeProvider } from '../context/localize'
|
||||||
import { ConfirmProvider } from '../context/confirm'
|
import { ConfirmProvider } from '../context/confirm'
|
||||||
import { EditorProvider } from '../context/editor'
|
import { EditorProvider } from '../context/editor'
|
||||||
|
import { NotificationsProvider } from '../context/notifications'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
||||||
|
@ -48,8 +49,6 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
||||||
author: AuthorPage,
|
author: AuthorPage,
|
||||||
authorComments: AuthorPage,
|
authorComments: AuthorPage,
|
||||||
authorAbout: AuthorPage,
|
authorAbout: AuthorPage,
|
||||||
authorFollowing: AuthorPage,
|
|
||||||
authorFollowers: AuthorPage,
|
|
||||||
inbox: InboxPage,
|
inbox: InboxPage,
|
||||||
expo: ExpoPage,
|
expo: ExpoPage,
|
||||||
expoLayout: ExpoPage,
|
expoLayout: ExpoPage,
|
||||||
|
@ -115,9 +114,11 @@ export const App = (props: PageProps) => {
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<EditorProvider>
|
<NotificationsProvider>
|
||||||
<Dynamic component={pageComponent()} {...props} />
|
<EditorProvider>
|
||||||
</EditorProvider>
|
<Dynamic component={pageComponent()} {...props} />
|
||||||
|
</EditorProvider>
|
||||||
|
</NotificationsProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</ConfirmProvider>
|
</ConfirmProvider>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
|
|
|
@ -5,19 +5,6 @@
|
||||||
.albumInfo {
|
.albumInfo {
|
||||||
margin-right: 224px;
|
margin-right: 224px;
|
||||||
|
|
||||||
.topic {
|
|
||||||
.link {
|
|
||||||
@include font-size(1.6rem);
|
|
||||||
|
|
||||||
color: var(--blue-link);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& > h1 {
|
& > h1 {
|
||||||
margin: 16px 0 0;
|
margin: 16px 0 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { MediaItem } from '../../../pages/types'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { Topic } from '../../../graphql/types.gen'
|
import { Topic } from '../../../graphql/types.gen'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { CardTopic } from '../../Feed/CardTopic'
|
||||||
import { router } from '../../../stores/router'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
|
@ -29,11 +28,7 @@ export const AudioHeader = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.albumInfo}>
|
<div class={styles.albumInfo}>
|
||||||
<Show when={props.topic}>
|
<Show when={props.topic}>
|
||||||
<div class={styles.topic}>
|
<CardTopic title={props.topic.title} slug={props.topic.slug} />
|
||||||
<a href={getPagePath(router, 'topic', { slug: props.topic.slug })} class={styles.link}>
|
|
||||||
{props.topic.title}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
<h1>{props.title}</h1>
|
<h1>{props.title}</h1>
|
||||||
<Show when={props.artistData}>
|
<Show when={props.artistData}>
|
||||||
|
|
|
@ -36,8 +36,9 @@ type Props = {
|
||||||
export const Comment = (props: Props) => {
|
export const Comment = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
|
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [editMode, setEditMode] = createSignal<boolean>(false)
|
const [editMode, setEditMode] = createSignal(false)
|
||||||
|
const [clearEditor, setClearEditor] = createSignal(false)
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -81,11 +82,13 @@ export const Comment = (props: Props) => {
|
||||||
body: value,
|
body: value,
|
||||||
shout: props.comment.shout.id
|
shout: props.comment.shout.id
|
||||||
})
|
})
|
||||||
|
setClearEditor(true)
|
||||||
setIsReplyVisible(false)
|
setIsReplyVisible(false)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleCreate reaction]:', error)
|
console.error('[handleCreate reaction]:', error)
|
||||||
}
|
}
|
||||||
|
setClearEditor(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleEditMode = () => {
|
const toggleEditMode = () => {
|
||||||
|
@ -175,11 +178,13 @@ export const Comment = (props: Props) => {
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
initialContent={comment().body}
|
initialContent={comment().body}
|
||||||
submitButtonText={t('Save')}
|
submitButtonText={t('Save')}
|
||||||
|
submitByEnter={true}
|
||||||
quoteEnabled={true}
|
quoteEnabled={true}
|
||||||
imageEnabled={true}
|
imageEnabled={true}
|
||||||
placeholder={t('Write a comment...')}
|
placeholder={t('Write a comment...')}
|
||||||
onSubmit={(value) => handleUpdate(value)}
|
onSubmit={(value) => handleUpdate(value)}
|
||||||
submitByShiftEnter={true}
|
submitByShiftEnter={true}
|
||||||
|
setClear={clearEditor()}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -33,7 +33,7 @@ const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
commentAuthors: Author[]
|
articleAuthors: Author[]
|
||||||
shoutSlug: string
|
shoutSlug: string
|
||||||
shoutId: number
|
shoutId: number
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,9 @@ export const CommentsTree = (props: Props) => {
|
||||||
{(reaction) => (
|
{(reaction) => (
|
||||||
<Comment
|
<Comment
|
||||||
sortedComments={sortedComments()}
|
sortedComments={sortedComments()}
|
||||||
isArticleAuthor={Boolean(props.commentAuthors.some((a) => a.slug === session()?.user.slug))}
|
isArticleAuthor={Boolean(
|
||||||
|
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug)
|
||||||
|
)}
|
||||||
comment={reaction}
|
comment={reaction}
|
||||||
lastSeen={dateFromLocalStorage}
|
lastSeen={dateFromLocalStorage}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -431,7 +431,7 @@ export const FullArticle = (props: Props) => {
|
||||||
<CommentsTree
|
<CommentsTree
|
||||||
shoutId={props.article.id}
|
shoutId={props.article.id}
|
||||||
shoutSlug={props.article.slug}
|
shoutSlug={props.article.slug}
|
||||||
commentAuthors={props.article.authors}
|
articleAuthors={props.article.authors}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -90,19 +90,22 @@
|
||||||
|
|
||||||
.authorName {
|
.authorName {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
display: block;
|
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 0.8rem;
|
||||||
|
|
||||||
.listWrapper &:before {
|
.listWrapper & {
|
||||||
content: '';
|
display: block;
|
||||||
height: 100%;
|
|
||||||
left: 0;
|
&:before {
|
||||||
position: absolute;
|
content: '';
|
||||||
top: 0;
|
height: 100%;
|
||||||
width: 100%;
|
left: 0;
|
||||||
z-index: 2;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +124,45 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.button {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.buttonUnfollowLabel {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonSubscribedLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonUnfollowLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorSubscribeSocial {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin: 2rem 0;
|
||||||
|
|
||||||
|
.socialLink {
|
||||||
border: none;
|
border: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
@ -162,251 +203,213 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='facebook.com/'] {
|
&[href*='facebook.com/'] {
|
||||||
&::before {
|
&::before {
|
||||||
background-image: url(/icons/user-link-facebook.svg);
|
background-image: url(/icons/user-link-facebook.svg);
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='twitter.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-twitter.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='telegram.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-telegram.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='vk.cc/'],
|
|
||||||
a[href*='vk.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-vk.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='tumblr.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-tumblr.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='instagram.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-instagram.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='behance.net/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-behance.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='dribbble.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-dribbble.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='github.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-github.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='linkedin.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-linkedin.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='medium.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-medium.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='ok.ru/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-ok.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='pinterest.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-pinterest.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='reddit.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-reddit.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='tiktok.com/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-tiktok.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='youtube.com/'],
|
|
||||||
a[href*='youtu.be/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-youtube.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a[href*='dzen.ru/'] {
|
|
||||||
&::before {
|
|
||||||
background-image: url(/icons/user-link-dzen.svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
padding-left: 2rem;
|
|
||||||
padding-right: 2rem;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.buttonUnfollowLabel {
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSubscribedLabel {
|
&:hover {
|
||||||
display: none;
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonUnfollowLabel {
|
&[href*='twitter.com/'] {
|
||||||
display: none;
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-twitter.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='telegram.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-telegram.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='vk.cc/'],
|
||||||
|
&[href*='vk.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-vk.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='tumblr.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-tumblr.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='instagram.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-instagram.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='behance.net/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-behance.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='dribbble.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-dribbble.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='github.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-github.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='linkedin.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-linkedin.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='medium.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-medium.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='ok.ru/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-ok.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='pinterest.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-pinterest.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='reddit.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-reddit.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='tiktok.com/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-tiktok.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='youtube.com/'],
|
||||||
|
&[href*='youtu.be/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-youtube.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[href*='dzen.ru/'] {
|
||||||
|
&::before {
|
||||||
|
background-image: url(/icons/user-link-dzen.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.authorSubscribeSocialLabel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.authorSubscribeSocialLabel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authorSubscribeSocial {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
margin: 0 0.8rem 2rem 0;
|
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
flex: 1 100%;
|
flex: 1 100%;
|
||||||
|
@ -470,21 +473,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWriteAuthorPage {
|
|
||||||
background: #f6f6f6 !important;
|
|
||||||
border-radius: 0.8rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #e9e9ee !important;
|
|
||||||
border-color: #e9e9ee;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #ccc !important;
|
|
||||||
border-color: #ccc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.authorPage {
|
.authorPage {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -637,7 +625,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -648,9 +637,9 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin-top: 1rem;
|
|
||||||
margin-right: 3rem;
|
margin-right: 3rem;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
border-bottom: unset !important;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
@ -679,10 +668,6 @@
|
||||||
.subscribersCounter {
|
.subscribersCounter {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-left: -0.6rem;
|
margin-left: -0.6rem;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #696969;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.listWrapper {
|
.listWrapper {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { Author } from '../../../graphql/types.gen'
|
||||||
import { Userpic } from '../Userpic'
|
import { Userpic } from '../Userpic'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import styles from './AuthorCard.module.scss'
|
import styles from './AuthorCard.module.scss'
|
||||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from 'solid-js'
|
||||||
import { translit } from '../../../utils/ru2en'
|
import { translit } from '../../../utils/ru2en'
|
||||||
import { follow, unfollow } from '../../../stores/zine/common'
|
import { follow, unfollow } from '../../../stores/zine/common'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -14,12 +14,13 @@ import { openPage, redirectPage } from '@nanostores/router'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||||
import { Modal } from '../../Nav/Modal'
|
import { Modal } from '../../Nav/Modal'
|
||||||
import { showModal } from '../../../stores/ui'
|
|
||||||
import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension'
|
|
||||||
import { SubscriptionFilter } from '../../../pages/types'
|
import { SubscriptionFilter } from '../../../pages/types'
|
||||||
import { isAuthor } from '../../../utils/isAuthor'
|
import { isAuthor } from '../../../utils/isAuthor'
|
||||||
import { AuthorBadge } from '../AuthorBadge'
|
import { AuthorBadge } from '../AuthorBadge'
|
||||||
import { TopicBadge } from '../../Topic/TopicBadge'
|
import { TopicBadge } from '../../Topic/TopicBadge'
|
||||||
|
import { Button } from '../../_shared/Button'
|
||||||
|
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||||
|
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
caption?: string
|
caption?: string
|
||||||
|
@ -43,11 +44,11 @@ type Props = {
|
||||||
following?: Array<Author | Topic>
|
following?: Array<Author | Topic>
|
||||||
showPublicationsCounter?: boolean
|
showPublicationsCounter?: boolean
|
||||||
hideBio?: boolean
|
hideBio?: boolean
|
||||||
|
isCurrentUser?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorCard = (props: Props) => {
|
export const AuthorCard = (props: Props) => {
|
||||||
const { t, lang } = useLocalize()
|
const { t, lang } = useLocalize()
|
||||||
const { page } = useRouter()
|
|
||||||
const {
|
const {
|
||||||
session,
|
session,
|
||||||
isSessionLoaded,
|
isSessionLoaded,
|
||||||
|
@ -117,12 +118,6 @@ export const AuthorCard = (props: Props) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (page().route === 'authorFollowing') {
|
|
||||||
showModal('following')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleCloseFollowModals = () => {
|
const handleCloseFollowModals = () => {
|
||||||
redirectPage(router, 'author', { slug: props.author.slug })
|
redirectPage(router, 'author', { slug: props.author.slug })
|
||||||
}
|
}
|
||||||
|
@ -130,191 +125,158 @@ export const AuthorCard = (props: Props) => {
|
||||||
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) {
|
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) {
|
||||||
setUserpicUrl(props.author.userpic.replace('100x', '500x500'))
|
setUserpicUrl(props.author.userpic.replace('100x', '500x500'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
|
class={clsx(styles.author, props.class)}
|
||||||
|
classList={{
|
||||||
|
['row']: props.isAuthorPage,
|
||||||
|
[styles.authorPage]: props.isAuthorPage,
|
||||||
|
[styles.authorComments]: props.isComments,
|
||||||
|
[styles.authorsListItem]: props.isAuthorsList,
|
||||||
|
[styles.feedMode]: props.isFeedMode,
|
||||||
|
[styles.nowrapView]: props.isNowrap
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={props.isAuthorPage}
|
||||||
|
fallback={
|
||||||
|
<Userpic
|
||||||
|
name={props.author.name}
|
||||||
|
userpic={props.author.userpic}
|
||||||
|
hasLink={props.hasLink}
|
||||||
|
isBig={props.isAuthorPage}
|
||||||
|
isAuthorsList={props.isAuthorsList}
|
||||||
|
isFeedMode={props.isFeedMode}
|
||||||
|
slug={props.author.slug}
|
||||||
|
class={styles.circlewrap}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<Userpic
|
||||||
|
name={props.author.name}
|
||||||
|
userpic={userpicUrl()}
|
||||||
|
hasLink={props.hasLink}
|
||||||
|
isBig={props.isAuthorPage}
|
||||||
|
isAuthorsList={props.isAuthorsList}
|
||||||
|
isFeedMode={props.isFeedMode}
|
||||||
|
slug={props.author.slug}
|
||||||
|
class={styles.circlewrap}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={clsx(styles.author, props.class)}
|
class={styles.authorDetails}
|
||||||
classList={{
|
classList={{
|
||||||
['row']: props.isAuthorPage,
|
'col-md-15 col-xl-13': props.isAuthorPage,
|
||||||
[styles.authorPage]: props.isAuthorPage,
|
[styles.authorDetailsShrinked]: props.isAuthorPage
|
||||||
[styles.authorComments]: props.isComments,
|
|
||||||
[styles.authorsListItem]: props.isAuthorsList,
|
|
||||||
[styles.feedMode]: props.isFeedMode,
|
|
||||||
[styles.nowrapView]: props.isNowrap
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Show
|
<div class={styles.authorDetailsWrapper}>
|
||||||
when={props.isAuthorPage}
|
<div class={styles.authorNameContainer}>
|
||||||
fallback={
|
<ConditionalWrapper
|
||||||
<Userpic
|
condition={props.hasLink}
|
||||||
name={props.author.name}
|
wrapper={(children) => (
|
||||||
userpic={props.author.userpic}
|
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
||||||
hasLink={props.hasLink}
|
{children}
|
||||||
isBig={props.isAuthorPage}
|
</a>
|
||||||
isAuthorsList={props.isAuthorsList}
|
)}
|
||||||
isFeedMode={props.isFeedMode}
|
>
|
||||||
slug={props.author.slug}
|
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
|
||||||
class={styles.circlewrap}
|
</ConditionalWrapper>
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="col-md-5">
|
|
||||||
<Userpic
|
|
||||||
name={props.author.name}
|
|
||||||
userpic={userpicUrl()}
|
|
||||||
hasLink={props.hasLink}
|
|
||||||
isBig={props.isAuthorPage}
|
|
||||||
isAuthorsList={props.isAuthorsList}
|
|
||||||
isFeedMode={props.isFeedMode}
|
|
||||||
slug={props.author.slug}
|
|
||||||
class={styles.circlewrap}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show
|
||||||
|
when={props.author.bio && !props.hideBio}
|
||||||
|
fallback={
|
||||||
|
props.showPublicationsCounter ? (
|
||||||
|
<div class={styles.authorAbout}>
|
||||||
|
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={styles.authorAbout}
|
||||||
|
classList={{ 'text-truncate': props.truncateBio }}
|
||||||
|
innerHTML={props.author.bio}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<div
|
<Show
|
||||||
class={styles.authorDetails}
|
when={
|
||||||
classList={{
|
(props.followers && props.followers.length > 0) ||
|
||||||
'col-md-15 col-xl-13': props.isAuthorPage,
|
(props.following && props.following.length > 0)
|
||||||
[styles.authorDetailsShrinked]: props.isAuthorPage
|
}
|
||||||
}}
|
>
|
||||||
>
|
<div class={styles.subscribersContainer}>
|
||||||
<div class={styles.authorDetailsWrapper}>
|
<Switch>
|
||||||
<div class={styles.authorNameContainer}>
|
<Match when={props.followers && props.followers.length > 0 && !props.isCurrentUser}>
|
||||||
<ConditionalWrapper
|
<a href="?modal=followers" class={styles.subscribers}>
|
||||||
condition={props.hasLink}
|
|
||||||
wrapper={(children) => (
|
|
||||||
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
|
|
||||||
</ConditionalWrapper>
|
|
||||||
</div>
|
|
||||||
<Show
|
|
||||||
when={props.author.bio && !props.hideBio}
|
|
||||||
fallback={
|
|
||||||
props.showPublicationsCounter ? (
|
|
||||||
<div class={styles.authorAbout}>
|
|
||||||
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class={styles.authorAbout}
|
|
||||||
classList={{ 'text-truncate': props.truncateBio }}
|
|
||||||
innerHTML={props.author.bio}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
(props.followers && props.followers.length > 0) ||
|
|
||||||
(props.following && props.following.length > 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class={styles.subscribersContainer}>
|
|
||||||
<Show when={props.followers && props.followers.length > 0}>
|
|
||||||
<div
|
|
||||||
class={styles.subscribers}
|
|
||||||
onClick={() => {
|
|
||||||
redirectPage(router, 'authorFollowers', { slug: props.author.slug })
|
|
||||||
showModal('followers')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={props.followers.slice(0, 3)}>
|
<For each={props.followers.slice(0, 3)}>
|
||||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||||
</For>
|
</For>
|
||||||
<div class={styles.subscribersCounter}>
|
<div class={styles.subscribersCounter}>
|
||||||
{props.followers.length}
|
{t('SubscriptionWithCount', { count: props.followers.length })}
|
||||||
{getNumeralsDeclension(props.followers.length, [
|
|
||||||
t('subscriber'),
|
|
||||||
t('subscriber_rp'),
|
|
||||||
t('subscribers')
|
|
||||||
])}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</Show>
|
</Match>
|
||||||
<Show when={props.following && props.following.length > 0}>
|
<Match when={props.followers && props.followers.length > 0 && props.isCurrentUser}>
|
||||||
<div
|
<Button
|
||||||
class={styles.subscribers}
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => redirectPage(router, 'profileSettings')}
|
||||||
redirectPage(router, 'authorFollowing', { slug: props.author.slug })
|
value={t('Edit profile')}
|
||||||
showModal('following')
|
/>
|
||||||
}}
|
</Match>
|
||||||
>
|
</Switch>
|
||||||
<For each={props.following.slice(0, 3)}>
|
|
||||||
{(f) => {
|
|
||||||
if ('name' in f) {
|
|
||||||
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
|
||||||
} else if ('title' in f) {
|
|
||||||
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
<div class={styles.subscribersCounter}>
|
|
||||||
{props.following.length}
|
|
||||||
{getNumeralsDeclension(props.following.length, [
|
|
||||||
t('subscription'),
|
|
||||||
t('subscription_rp'),
|
|
||||||
t('subscriptions')
|
|
||||||
])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<ShowOnlyOnClient>
|
|
||||||
<Show when={isSessionLoaded()}>
|
|
||||||
<Show when={canFollow()}>
|
|
||||||
<div class={styles.authorSubscribe}>
|
|
||||||
<Show when={!props.noSocialButtons && !props.hideWriteButton && props.author.links}>
|
|
||||||
<div class={styles.authorSubscribeSocial}>
|
|
||||||
<For each={props.author.links}>
|
|
||||||
{(link) => (
|
|
||||||
<a href={link}>
|
|
||||||
<span class={styles.authorSubscribeSocialLabel}>{link}</span>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
<Show when={props.following && props.following.length > 0}>
|
||||||
when={subscribed()}
|
<a href="?modal=following" class={styles.subscribers}>
|
||||||
fallback={
|
<For each={props.following.slice(0, 3)}>
|
||||||
<button
|
{(f) => {
|
||||||
onClick={handleSubscribe}
|
if ('name' in f) {
|
||||||
class={clsx('button', styles.button)}
|
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
||||||
classList={{
|
} else if ('title' in f) {
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
||||||
'button--subscribe': !props.isAuthorsList,
|
}
|
||||||
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
return null
|
||||||
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
}}
|
||||||
[styles.isSubscribing]: isSubscribing()
|
</For>
|
||||||
}}
|
<div class={styles.subscribersCounter}>
|
||||||
disabled={isSubscribing()}
|
{t('SubscriberWithCount', { count: props?.following.length ?? 0 })}
|
||||||
>
|
</div>
|
||||||
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
</a>
|
||||||
<Icon name="author-subscribe" class={styles.icon} />
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
<Show when={props.isTextButton || props.isAuthorPage}>
|
</Show>
|
||||||
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
|
</div>
|
||||||
{t('Follow')}
|
<ShowOnlyOnClient>
|
||||||
</span>
|
<Show when={isSessionLoaded() && props.author.links}>
|
||||||
</Show>
|
<div class={styles.authorSubscribeSocial}>
|
||||||
</button>
|
<For each={props.author.links}>
|
||||||
}
|
{(link) => (
|
||||||
|
<a
|
||||||
|
class={styles.socialLink}
|
||||||
|
href={link.startsWith('http') ? link : `https://${link}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="nofollow noopener"
|
||||||
>
|
>
|
||||||
|
<span class={styles.authorSubscribeSocialLabel}>
|
||||||
|
{link.startsWith('http') ? link : `https://${link}`}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Show when={canFollow()}>
|
||||||
|
<div class={styles.authorSubscribe}>
|
||||||
|
<Show
|
||||||
|
when={subscribed()}
|
||||||
|
fallback={
|
||||||
<button
|
<button
|
||||||
onClick={() => subscribe(false)}
|
onClick={handleSubscribe}
|
||||||
class={clsx('button', styles.button)}
|
class={clsx('button', styles.button)}
|
||||||
classList={{
|
classList={{
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||||
|
@ -326,119 +288,152 @@ export const AuthorCard = (props: Props) => {
|
||||||
disabled={isSubscribing()}
|
disabled={isSubscribing()}
|
||||||
>
|
>
|
||||||
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||||
<Icon name="author-unsubscribe" class={styles.icon} />
|
<Icon name="author-subscribe" class={styles.icon} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.isTextButton || props.isAuthorPage}>
|
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||||
<span
|
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
|
||||||
class={clsx(
|
{t('Follow')}
|
||||||
styles.buttonLabel,
|
|
||||||
styles.buttonLabelVisible,
|
|
||||||
styles.buttonUnfollowLabel
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('Unfollow')}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class={clsx(
|
|
||||||
styles.buttonLabel,
|
|
||||||
styles.buttonLabelVisible,
|
|
||||||
styles.buttonSubscribedLabel
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t('You are subscribed')}
|
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => subscribe(false)}
|
||||||
|
class={clsx('button', styles.button)}
|
||||||
|
classList={{
|
||||||
|
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||||
|
'button--subscribe': !props.isAuthorsList,
|
||||||
|
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
|
>
|
||||||
|
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||||
|
<Icon name="author-unsubscribe" class={styles.icon} />
|
||||||
|
</Show>
|
||||||
|
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
styles.buttonLabel,
|
||||||
|
styles.buttonLabelVisible,
|
||||||
|
styles.buttonUnfollowLabel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('Unfollow')}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
styles.buttonLabel,
|
||||||
|
styles.buttonLabelVisible,
|
||||||
|
styles.buttonSubscribedLabel
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('You are subscribed')}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.hideWriteButton}>
|
<Show when={!props.hideWriteButton}>
|
||||||
<button
|
<button
|
||||||
class={styles.button}
|
class={clsx(styles.button, styles.buttonSubscribe)}
|
||||||
classList={{
|
classList={{
|
||||||
[styles.buttonWriteAuthorPage]: !props.isAuthorsList,
|
'button--subscribe': !props.isAuthorsList,
|
||||||
'button--subscribe': !props.isAuthorsList,
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
}}
|
||||||
}}
|
onClick={initChat}
|
||||||
onClick={initChat}
|
>
|
||||||
>
|
<Show when={!props.isTextButton && !props.isAuthorPage}>
|
||||||
<Show when={!props.isTextButton && !props.isAuthorPage}>
|
<Icon name="comment" class={styles.icon} />
|
||||||
<Icon name="comment" class={styles.icon} />
|
</Show>
|
||||||
</Show>
|
<Show when={!props.liteButtons || props.isTextButton}>{t('Write')}</Show>
|
||||||
<Show when={!props.liteButtons || props.isTextButton}>{t('Write')}</Show>
|
</button>
|
||||||
</button>
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
</Show>
|
||||||
</ShowOnlyOnClient>
|
</Show>
|
||||||
</div>
|
</ShowOnlyOnClient>
|
||||||
|
<Show when={props.followers}>
|
||||||
|
<Modal variant="medium" name="followers" onClose={handleCloseFollowModals} maxHeight>
|
||||||
|
<>
|
||||||
|
<h2>{t('Followers')}</h2>
|
||||||
|
<div class={styles.listWrapper}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-24">
|
||||||
|
<For each={props.followers}>
|
||||||
|
{(follower: Author) => <AuthorBadge author={follower} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.isCurrentUser}>
|
||||||
|
<div class={styles.subscribersContainer}>
|
||||||
|
<SharePopup
|
||||||
|
containerCssClass={stylesHeader.control}
|
||||||
|
title={props.author.name}
|
||||||
|
description={props.author.bio}
|
||||||
|
imageUrl={props.author.userpic}
|
||||||
|
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
|
||||||
|
trigger={<Button variant="secondary" value={t('Share')} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={props.following}>
|
||||||
|
<Modal variant="medium" name="following" onClose={handleCloseFollowModals} maxHeight>
|
||||||
|
<>
|
||||||
|
<h2>{t('Subscriptions')}</h2>
|
||||||
|
<ul class="view-switcher">
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||||
|
{t('All')}
|
||||||
|
</button>
|
||||||
|
<span class="view-switcher__counter">{props.following.length}</span>
|
||||||
|
</li>
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
||||||
|
{t('Users')}
|
||||||
|
</button>
|
||||||
|
<span class="view-switcher__counter">
|
||||||
|
{props.following.filter((s) => 'name' in s).length}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
||||||
|
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||||
|
{t('Topics')}
|
||||||
|
</button>
|
||||||
|
<span class="view-switcher__counter">
|
||||||
|
{props.following.filter((s) => 'title' in s).length}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<div class={styles.listWrapper}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-24">
|
||||||
|
<For each={following()}>
|
||||||
|
{(subscription) =>
|
||||||
|
isAuthor(subscription) ? (
|
||||||
|
<AuthorBadge author={subscription} />
|
||||||
|
) : (
|
||||||
|
<TopicBadge topic={subscription} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Show when={props.followers}>
|
|
||||||
<Modal variant="medium" name="followers" onClose={handleCloseFollowModals} maxHeight>
|
|
||||||
<>
|
|
||||||
<h2>{t('Followers')}</h2>
|
|
||||||
<div class={styles.listWrapper}>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-24">
|
|
||||||
<For each={props.followers}>
|
|
||||||
{(follower: Author) => <AuthorBadge author={follower} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Modal>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={props.following}>
|
|
||||||
<Modal variant="medium" name="following" onClose={handleCloseFollowModals} maxHeight>
|
|
||||||
<>
|
|
||||||
<h2>{t('Subscriptions')}</h2>
|
|
||||||
<ul class="view-switcher">
|
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
|
||||||
{t('All')}
|
|
||||||
</button>
|
|
||||||
<span class="view-switcher__counter">{props.following.length}</span>
|
|
||||||
</li>
|
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
|
||||||
{t('Users')}
|
|
||||||
</button>
|
|
||||||
<span class="view-switcher__counter">
|
|
||||||
{props.following.filter((s) => 'name' in s).length}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
|
||||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
|
||||||
{t('Topics')}
|
|
||||||
</button>
|
|
||||||
<span class="view-switcher__counter">
|
|
||||||
{props.following.filter((s) => 'title' in s).length}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<br />
|
|
||||||
<div class={styles.listWrapper}>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-24">
|
|
||||||
<For each={following()}>
|
|
||||||
{(subscription) =>
|
|
||||||
isAuthor(subscription) ? (
|
|
||||||
<AuthorBadge author={subscription} />
|
|
||||||
) : (
|
|
||||||
<TopicBadge topic={subscription} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</Modal>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
&.big {
|
&.big {
|
||||||
aspect-ratio: 1/1;
|
aspect-ratio: 1/1;
|
||||||
margin: 0 auto;
|
margin: 0 auto 1rem;
|
||||||
max-width: 168px;
|
max-width: 168px;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import '../../styles/help.scss'
|
import '../../styles/help.scss'
|
||||||
import { createSignal, onMount } from 'solid-js'
|
import { createSignal, onMount } from 'solid-js'
|
||||||
import { showModal, warn } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { useSnackbar } from '../../context/snackbar'
|
||||||
|
|
||||||
export const Donate = () => {
|
export const Donate = () => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -20,6 +21,9 @@ export const Donate = () => {
|
||||||
const [showingPayment, setShowingPayment] = createSignal<boolean>()
|
const [showingPayment, setShowingPayment] = createSignal<boolean>()
|
||||||
const [period, setPeriod] = createSignal(monthly)
|
const [period, setPeriod] = createSignal(monthly)
|
||||||
const [amount, setAmount] = createSignal(0)
|
const [amount, setAmount] = createSignal(0)
|
||||||
|
const {
|
||||||
|
actions: { showSnackbar }
|
||||||
|
} = useSnackbar()
|
||||||
|
|
||||||
const initiated = () => {
|
const initiated = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -104,10 +108,10 @@ export const Donate = () => {
|
||||||
// fail
|
// fail
|
||||||
// действие при неуспешной оплате
|
// действие при неуспешной оплате
|
||||||
console.debug('[donate] options', options)
|
console.debug('[donate] options', options)
|
||||||
warn({
|
|
||||||
kind: 'error',
|
showSnackbar({
|
||||||
body: reason,
|
type: 'error',
|
||||||
seen: false
|
body: reason
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -250,7 +250,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mainNavigationItemActive {
|
.mainNavigationItemActive {
|
||||||
background: var(--link-hover-background);
|
background: var(--link-hover-background) !important;
|
||||||
color: var(--link-hover-color) !important;
|
color: var(--link-hover-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,9 @@ import { clsx } from 'clsx'
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
import Notifications from './Notifications'
|
|
||||||
import { ProfilePopup } from './ProfilePopup'
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
import { Userpic } from '../Author/Userpic'
|
import { Userpic } from '../Author/Userpic'
|
||||||
import { showModal, useWarningsStore } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
|
@ -14,6 +13,7 @@ import { getPagePath } from '@nanostores/router'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { useEditorContext } from '../../context/editor'
|
import { useEditorContext } from '../../context/editor'
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
|
import { useNotifications } from '../../context/notifications'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setIsProfilePopupVisible: (value: boolean) => void
|
setIsProfilePopupVisible: (value: boolean) => void
|
||||||
|
@ -29,18 +29,17 @@ const MD_WIDTH_BREAKPOINT = 992
|
||||||
export const HeaderAuth = (props: Props) => {
|
export const HeaderAuth = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
|
||||||
const { warnings } = useWarningsStore()
|
|
||||||
|
|
||||||
const { session, isSessionLoaded, isAuthenticated } = useSession()
|
const { session, isSessionLoaded, isAuthenticated } = useSession()
|
||||||
|
const {
|
||||||
|
unreadNotificationsCount,
|
||||||
|
actions: { showNotificationsPanel }
|
||||||
|
} = useNotifications()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
actions: { toggleEditorPanel, saveShout, publishShout }
|
actions: { toggleEditorPanel, saveShout, publishShout }
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
|
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
|
||||||
|
|
||||||
const handleBellIconClick = (event: Event) => {
|
const handleBellIconClick = (event: Event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
|
@ -48,15 +47,16 @@ export const HeaderAuth = (props: Props) => {
|
||||||
showModal('auth')
|
showModal('auth')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
toggleWarnings()
|
|
||||||
|
showNotificationsPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings')
|
||||||
|
|
||||||
const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||||
const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage())
|
const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage())
|
||||||
const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage())
|
const isCreatePostButtonVisible = createMemo(() => isAuthenticated() && !isEditorPage())
|
||||||
const showAuthenticatedControls = createMemo(() => isAuthenticated())
|
const isAuthenticatedControlsVisible = createMemo(() => isAuthenticated())
|
||||||
|
|
||||||
const handleBurgerButtonClick = () => {
|
const handleBurgerButtonClick = () => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
|
@ -71,8 +71,9 @@ export const HeaderAuth = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [width, setWidth] = createSignal(0)
|
const [width, setWidth] = createSignal(0)
|
||||||
const handleResize = () => setWidth(window.innerWidth)
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
const handleResize = () => setWidth(window.innerWidth)
|
||||||
handleResize()
|
handleResize()
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
onCleanup(() => window.removeEventListener('resize', handleResize))
|
onCleanup(() => window.removeEventListener('resize', handleResize))
|
||||||
|
@ -109,7 +110,7 @@ export const HeaderAuth = (props: Props) => {
|
||||||
<Show when={isSessionLoaded()} keyed={true}>
|
<Show when={isSessionLoaded()} keyed={true}>
|
||||||
<div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
|
<div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
|
||||||
<div class={styles.userControl}>
|
<div class={styles.userControl}>
|
||||||
<Show when={showCreatePostButton()}>
|
<Show when={isCreatePostButtonVisible()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
<a href={getPagePath(router, 'create')}>
|
<a href={getPagePath(router, 'create')}>
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
@ -126,26 +127,19 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={showNotifications()}>
|
<Show when={isNotificationsVisible()}>
|
||||||
<div class={styles.userControlItem}>
|
<div class={styles.userControlItem} onClick={handleBellIconClick}>
|
||||||
<a href="#" onClick={handleBellIconClick}>
|
{/*TODO: check markup (cursor: pointer, hover)*/}
|
||||||
<div>
|
<Icon name="bell-white" counter={unreadNotificationsCount()} class={styles.icon} />
|
||||||
<Icon
|
<Icon
|
||||||
name="bell-white"
|
name="bell-white-hover"
|
||||||
counter={isAuthenticated() ? warnings().length : 1}
|
counter={unreadNotificationsCount()}
|
||||||
class={styles.icon}
|
class={clsx(styles.icon, styles.iconHover)}
|
||||||
/>
|
/>
|
||||||
<Icon
|
|
||||||
name="bell-white-hover"
|
|
||||||
counter={isAuthenticated() ? warnings().length : 1}
|
|
||||||
class={clsx(styles.icon, styles.iconHover)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={showSaveButton()}>
|
<Show when={isSaveButtonVisible()}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
{renderIconedButton({
|
{renderIconedButton({
|
||||||
value: t('Save'),
|
value: t('Save'),
|
||||||
|
@ -175,15 +169,8 @@ export const HeaderAuth = (props: Props) => {
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={visibleWarnings()}>
|
|
||||||
<div class={clsx(styles.userControlItem, 'notifications')}>
|
|
||||||
<Notifications />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={showAuthenticatedControls()}
|
when={isAuthenticatedControlsVisible()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
<a href="?modal=auth&mode=login">
|
<a href="?modal=auth&mode=login">
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
||||||
import styles from './Modal.module.scss'
|
import styles from './Modal.module.scss'
|
||||||
import { redirectPage } from '@nanostores/router'
|
import { redirectPage } from '@nanostores/router'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
|
import { Icon } from '../../_shared/Icon'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string
|
||||||
|
@ -55,18 +56,7 @@ export const Modal = (props: Props) => {
|
||||||
>
|
>
|
||||||
<div class={styles.modalInner}>{props.children}</div>
|
<div class={styles.modalInner}>{props.children}</div>
|
||||||
<div class={styles.close} onClick={handleHide}>
|
<div class={styles.close} onClick={handleHide}>
|
||||||
<svg
|
<Icon name="close" class={styles.icon} />
|
||||||
class={styles.icon}
|
|
||||||
width="16"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 16 18"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M7.99987 7.52552L14.1871 0.92334L15.9548 2.80968L9.76764 9.41185L15.9548 16.014L14.1871 17.9004L7.99987 11.2982L1.81269 17.9004L0.0449219 16.014L6.23211 9.41185L0.0449225 2.80968L1.81269 0.92334L7.99987 7.52552Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Portal } from 'solid-js/web'
|
|
||||||
import { useWarningsStore } from '../../stores/ui'
|
|
||||||
import { createMemo, For, Show } from 'solid-js'
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const { warnings } = useWarningsStore()
|
|
||||||
|
|
||||||
const notSeen = createMemo(() => warnings().filter((warning) => !warning.seen))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Show when={notSeen().length > 0}>
|
|
||||||
<Portal>
|
|
||||||
<ul class="warns">
|
|
||||||
<For each={warnings()}>{(warning) => <li>{warning.body}</li>}</For>
|
|
||||||
</ul>
|
|
||||||
</Portal>
|
|
||||||
</Show>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -26,7 +26,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
<a href={getPagePath(router, 'drafts')}>{t('Drafts')}</a>
|
<a href={getPagePath(router, 'drafts')}>{t('Drafts')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href={`${getPagePath(router, 'authorFollowing', { slug: user().slug })}`}>
|
<a href={`${getPagePath(router, 'author', { slug: user().slug })}?modal=following`}>
|
||||||
{t('Subscriptions')}
|
{t('Subscriptions')}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
.NotificationView {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 72px;
|
||||||
|
margin-left: -16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: var(--yellow-50);
|
||||||
|
// TODO: check markup
|
||||||
|
font-size: 15px;
|
||||||
|
// font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 100ms;
|
||||||
|
|
||||||
|
&.seen {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.userpic {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeContainer {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './NotificationView.module.scss'
|
||||||
|
import type { Notification } from '../../../graphql/types.gen'
|
||||||
|
import { formatDate } from '../../../utils'
|
||||||
|
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||||
|
import { NotificationType } from '../../../graphql/types.gen'
|
||||||
|
import { openPage } from '@nanostores/router'
|
||||||
|
import { router } from '../../../stores/router'
|
||||||
|
import { useNotifications } from '../../../context/notifications'
|
||||||
|
import { Userpic } from '../../Author/Userpic'
|
||||||
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
import notifications from '../../../graphql/query/notifications'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
notification: Notification
|
||||||
|
onClick: () => void
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationData = {
|
||||||
|
shout: {
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
users: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
slug: string
|
||||||
|
userpic: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationView = (props: Props) => {
|
||||||
|
const {
|
||||||
|
actions: { markNotificationAsRead }
|
||||||
|
} = useNotifications()
|
||||||
|
|
||||||
|
const { t } = useLocalize()
|
||||||
|
|
||||||
|
const [data, setData] = createSignal<NotificationData>(null)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(() => setData(JSON.parse(props.notification.data)))
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastUser = createMemo(() => {
|
||||||
|
if (!data()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return data().users[data().users.length - 1]
|
||||||
|
})
|
||||||
|
|
||||||
|
const content = createMemo(() => {
|
||||||
|
if (!data()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let shoutTitle = ''
|
||||||
|
let i = 0
|
||||||
|
const shoutTitleWords = data().shout.title.split(' ')
|
||||||
|
|
||||||
|
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
|
||||||
|
shoutTitle += shoutTitleWords[i] + ' '
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shoutTitle.length < data().shout.title.length) {
|
||||||
|
shoutTitle += '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (props.notification.type) {
|
||||||
|
case NotificationType.NewComment: {
|
||||||
|
return t('NewCommentNotificationText', {
|
||||||
|
commentsCount: props.notification.occurrences,
|
||||||
|
shoutTitle,
|
||||||
|
lastCommenterName: lastUser().name,
|
||||||
|
restUsersCount: data().users.length - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case NotificationType.NewReply: {
|
||||||
|
return t('NewReplyNotificationText', {
|
||||||
|
commentsCount: props.notification.occurrences,
|
||||||
|
shoutTitle,
|
||||||
|
lastCommenterName: lastUser().name,
|
||||||
|
restUsersCount: data().users.length - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!props.notification.seen) {
|
||||||
|
markNotificationAsRead(props.notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
openPage(router, 'article', { slug: data().shout.slug })
|
||||||
|
props.onClick()
|
||||||
|
|
||||||
|
// switch (props.notification.type) {
|
||||||
|
// case NotificationType.NewComment: {
|
||||||
|
// openPage(router, 'article', { slug: data().shout.slug })
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// case NotificationType.NewReply: {
|
||||||
|
// openPage(router, 'article', { slug: data().shout.slug })
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={data()}>
|
||||||
|
<div
|
||||||
|
class={clsx(styles.NotificationView, props.class, {
|
||||||
|
[styles.seen]: props.notification.seen
|
||||||
|
})}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Userpic name={lastUser().name} userpic={lastUser().userpic} class={styles.userpic} />
|
||||||
|
<div>{content()}</div>
|
||||||
|
<div class={styles.timeContainer}>
|
||||||
|
{/*{formatDate(new Date(props.notification.createdAt), { month: 'numeric' })}*/}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export { NotificationView } from './NotificationView'
|
|
@ -0,0 +1,66 @@
|
||||||
|
$transition-duration: 200ms;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
background-color: rgb(0 0 0 / 0%);
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
background-color $transition-duration,
|
||||||
|
width 0ms linear $transition-duration;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
position: relative;
|
||||||
|
background-color: #fff;
|
||||||
|
width: 700px;
|
||||||
|
padding: 48px 96px 96px 48px;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform $transition-duration;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isOpened {
|
||||||
|
width: 100%;
|
||||||
|
background-color: rgb(0 0 0 / 60%);
|
||||||
|
transition:
|
||||||
|
background-color $transition-duration,
|
||||||
|
width 0ms;
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
// TODO: check markup
|
||||||
|
color: var(--black-500, #141414);
|
||||||
|
font-size: 32px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 36px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notificationView + .notificationView {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyMessageContainer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
70
src/components/NotificationsPanel/NotificationsPanel.tsx
Normal file
70
src/components/NotificationsPanel/NotificationsPanel.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { clsx } from 'clsx'
|
||||||
|
import styles from './NotificationsPanel.module.scss'
|
||||||
|
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
||||||
|
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||||
|
import { useLocalize } from '../../context/localize'
|
||||||
|
import { Icon } from '../_shared/Icon'
|
||||||
|
import { createEffect, For, onCleanup, onMount } from 'solid-js'
|
||||||
|
import { useNotifications } from '../../context/notifications'
|
||||||
|
import { NotificationView } from './NotificationView'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotificationsPanel = (props: Props) => {
|
||||||
|
const { t } = useLocalize()
|
||||||
|
const { sortedNotifications } = useNotifications()
|
||||||
|
const handleHide = () => {
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelRef: { current: HTMLDivElement } = {
|
||||||
|
current: null
|
||||||
|
}
|
||||||
|
|
||||||
|
useOutsideClickHandler({
|
||||||
|
containerRef: panelRef,
|
||||||
|
predicate: () => props.isOpen,
|
||||||
|
handler: () => handleHide()
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
document.body.classList.toggle('fixed', props.isOpen)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEscKeyDownHandler(handleHide)
|
||||||
|
|
||||||
|
const handleNotificationViewClick = () => {
|
||||||
|
handleHide()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(styles.container, {
|
||||||
|
[styles.isOpened]: props.isOpen
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div ref={(el) => (panelRef.current = el)} class={styles.panel}>
|
||||||
|
<div class={styles.closeButton} onClick={handleHide}>
|
||||||
|
{/*TODO: check markup (hover)*/}
|
||||||
|
<Icon name="close" />
|
||||||
|
</div>
|
||||||
|
<div class={styles.title}>{t('Notifications')}</div>
|
||||||
|
<For
|
||||||
|
each={sortedNotifications()}
|
||||||
|
fallback={<div class={styles.emptyMessageContainer}>{t('No notifications, yet')}</div>}
|
||||||
|
>
|
||||||
|
{(notification) => (
|
||||||
|
<NotificationView
|
||||||
|
notification={notification}
|
||||||
|
class={styles.notificationView}
|
||||||
|
onClick={handleNotificationViewClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
1
src/components/NotificationsPanel/index.ts
Normal file
1
src/components/NotificationsPanel/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { NotificationsPanel } from './NotificationsPanel'
|
|
@ -60,7 +60,7 @@
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.2rem;
|
||||||
margin-top: 0.5rem !important;
|
margin-top: 0.5rem !important;
|
||||||
|
|
||||||
a {
|
a:link {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorHeader {
|
.authorHeader {
|
||||||
border-bottom: 2px solid #000;
|
border-bottom: 2px solid var(--default-color);
|
||||||
margin-bottom: 2.4rem;
|
margin-bottom: 2.4rem;
|
||||||
margin-top: -3.2rem;
|
margin-top: 1.8rem;
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
|
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
margin-top: -3.2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratingContainer {
|
.ratingContainer {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { useLocalize } from '../../../context/localize'
|
||||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
import { useSession } from '../../../context/session'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shouts: Shout[]
|
shouts: Shout[]
|
||||||
|
@ -33,7 +34,8 @@ export const AuthorView = (props: Props) => {
|
||||||
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
|
||||||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||||
|
|
||||||
const { page } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
const { user } = useSession()
|
||||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||||
|
@ -102,14 +104,14 @@ export const AuthorView = (props: Props) => {
|
||||||
// return t('Top recent')
|
// return t('Top recent')
|
||||||
// })
|
// })
|
||||||
|
|
||||||
const shouts = createMemo<Shout[][]>(() =>
|
const pages = createMemo<Shout[][]>(() =>
|
||||||
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||||
)
|
)
|
||||||
|
|
||||||
const [commented, setCommented] = createSignal([])
|
const [commented, setCommented] = createSignal([])
|
||||||
|
|
||||||
createEffect(async () => {
|
createEffect(async () => {
|
||||||
if (page().route === 'authorComments') {
|
if (getPage().route === 'authorComments') {
|
||||||
try {
|
try {
|
||||||
const data = await apiClient.getReactionsBy({
|
const data = await apiClient.getReactionsBy({
|
||||||
by: { comment: true, createdBy: props.authorSlug }
|
by: { comment: true, createdBy: props.authorSlug }
|
||||||
|
@ -131,23 +133,24 @@ export const AuthorView = (props: Props) => {
|
||||||
isAuthorPage={true}
|
isAuthorPage={true}
|
||||||
followers={followers()}
|
followers={followers()}
|
||||||
following={following()}
|
following={following()}
|
||||||
|
isCurrentUser={author().slug === user()?.slug}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={clsx(styles.groupControls, 'row')}>
|
<div class={clsx(styles.groupControls, 'row')}>
|
||||||
<div class="col-md-16">
|
<div class="col-md-16">
|
||||||
<ul class="view-switcher">
|
<ul class="view-switcher">
|
||||||
<li classList={{ 'view-switcher__item--selected': page().route === 'author' }}>
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'author' }}>
|
||||||
<a href={getPagePath(router, 'author', { slug: props.authorSlug })}>{t('Publications')}</a>
|
<a href={getPagePath(router, 'author', { slug: props.authorSlug })}>{t('Publications')}</a>
|
||||||
<span class="view-switcher__counter">{author().stat.shouts}</span>
|
<span class="view-switcher__counter">{author().stat?.shouts}</span>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ 'view-switcher__item--selected': page().route === 'authorComments' }}>
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorComments' }}>
|
||||||
<a href={getPagePath(router, 'authorComments', { slug: props.authorSlug })}>
|
<a href={getPagePath(router, 'authorComments', { slug: props.authorSlug })}>
|
||||||
{t('Comments')}
|
{t('Comments')}
|
||||||
</a>
|
</a>
|
||||||
<span class="view-switcher__counter">{author().stat.commented}</span>
|
<span class="view-switcher__counter">{author().stat?.commented}</span>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ 'view-switcher__item--selected': page().route === 'authorAbout' }}>
|
<li classList={{ 'view-switcher__item--selected': getPage().route === 'authorAbout' }}>
|
||||||
<a
|
<a
|
||||||
onClick={() => checkBioHeight()}
|
onClick={() => checkBioHeight()}
|
||||||
href={getPagePath(router, 'authorAbout', { slug: props.authorSlug })}
|
href={getPagePath(router, 'authorAbout', { slug: props.authorSlug })}
|
||||||
|
@ -167,7 +170,7 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={page().route === 'authorAbout'}>
|
<Match when={getPage().route === 'authorAbout'}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-20 col-lg-18">
|
<div class="col-md-20 col-lg-18">
|
||||||
|
@ -191,7 +194,7 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={page().route === 'authorComments'}>
|
<Match when={getPage().route === 'authorComments'}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-20 col-lg-18">
|
<div class="col-md-20 col-lg-18">
|
||||||
|
@ -204,8 +207,7 @@ export const AuthorView = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={getPage().route === 'author'}>
|
||||||
<Match when={page().route === 'author'}>
|
|
||||||
<Show when={sortedArticles().length === 1}>
|
<Show when={sortedArticles().length === 1}>
|
||||||
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -226,15 +228,15 @@ export const AuthorView = (props: Props) => {
|
||||||
<Row1 article={sortedArticles()[6]} noauthor={true} nodate={true} />
|
<Row1 article={sortedArticles()[6]} noauthor={true} nodate={true} />
|
||||||
<Row2 articles={sortedArticles().slice(7, 9)} isEqual={true} noauthor={true} />
|
<Row2 articles={sortedArticles().slice(7, 9)} isEqual={true} noauthor={true} />
|
||||||
|
|
||||||
<For each={shouts()}>
|
<For each={pages()}>
|
||||||
{(shout) => (
|
{(page) => (
|
||||||
<>
|
<>
|
||||||
<Row1 article={shout[0]} noauthor={true} nodate={true} />
|
<Row1 article={page[0]} noauthor={true} nodate={true} />
|
||||||
<Row2 articles={shout.slice(1, 3)} isEqual={true} noauthor={true} />
|
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
|
||||||
<Row1 article={shout[3]} noauthor={true} nodate={true} />
|
<Row1 article={page[3]} noauthor={true} nodate={true} />
|
||||||
<Row2 articles={shout.slice(4, 6)} isEqual={true} noauthor={true} />
|
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
|
||||||
<Row1 article={shout[6]} noauthor={true} nodate={true} />
|
<Row1 article={page[6]} noauthor={true} nodate={true} />
|
||||||
<Row2 articles={shout.slice(7, 9)} isEqual={true} noauthor={true} />
|
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.withPadding {
|
main.withPadding {
|
||||||
padding-top: 58px;
|
padding-top: 58px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const PageLayout = (props: Props) => {
|
||||||
props.scrollToComments(scrollToComments())
|
props.scrollToComments(scrollToComments())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// const { randomTopics } = useTopicsStore()
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
<Meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
|
@ -73,7 +73,8 @@
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a:link,
|
||||||
|
:global(.link) {
|
||||||
border: none;
|
border: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@ -100,7 +101,9 @@
|
||||||
|
|
||||||
.shareControl {
|
.shareControl {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: color 0.3s, background-color 0.3s;
|
transition:
|
||||||
|
color 0.3s,
|
||||||
|
background-color 0.3s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
106
src/context/notifications.tsx
Normal file
106
src/context/notifications.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import type { Accessor, JSX } from 'solid-js'
|
||||||
|
import { createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
|
||||||
|
import { useSession } from './session'
|
||||||
|
import SSEService, { EventData } from '../utils/sseService'
|
||||||
|
import { apiBaseUrl } from '../utils/config'
|
||||||
|
import { Portal } from 'solid-js/web'
|
||||||
|
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
||||||
|
import { NotificationsPanel } from '../components/NotificationsPanel'
|
||||||
|
import { apiClient } from '../utils/apiClient'
|
||||||
|
import { createStore } from 'solid-js/store'
|
||||||
|
import { Notification } from '../graphql/types.gen'
|
||||||
|
|
||||||
|
type NotificationsContextType = {
|
||||||
|
notificationEntities: Record<number, Notification>
|
||||||
|
unreadNotificationsCount: Accessor<number>
|
||||||
|
sortedNotifications: Accessor<Notification[]>
|
||||||
|
actions: {
|
||||||
|
showNotificationsPanel: () => void
|
||||||
|
markNotificationAsRead: (notification: Notification) => Promise<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NotificationsContext = createContext<NotificationsContextType>()
|
||||||
|
|
||||||
|
export function useNotifications() {
|
||||||
|
return useContext(NotificationsContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sseService = new SSEService()
|
||||||
|
|
||||||
|
export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
||||||
|
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
|
||||||
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
||||||
|
const { isAuthenticated, user } = useSession()
|
||||||
|
const [notificationEntities, setNotificationEntities] = createStore<Record<number, Notification>>({})
|
||||||
|
|
||||||
|
const loadNotifications = async () => {
|
||||||
|
const { notifications, totalUnreadCount } = await apiClient.getNotifications({
|
||||||
|
limit: 100
|
||||||
|
})
|
||||||
|
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
||||||
|
acc[notification.id] = notification
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
setUnreadNotificationsCount(totalUnreadCount)
|
||||||
|
setNotificationEntities(newNotificationEntities)
|
||||||
|
return notifications
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedNotifications = createMemo(() => {
|
||||||
|
return Object.values(notificationEntities).sort(
|
||||||
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (isAuthenticated()) {
|
||||||
|
loadNotifications()
|
||||||
|
|
||||||
|
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`)
|
||||||
|
sseService.subscribeToEvent('message', (data: EventData) => {
|
||||||
|
if (data.type === 'newNotifications') {
|
||||||
|
loadNotifications()
|
||||||
|
} else {
|
||||||
|
console.error(`[NotificationsProvider] unknown message type: ${JSON.stringify(data)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
sseService.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const markNotificationAsRead = async (notification: Notification) => {
|
||||||
|
await apiClient.markNotificationAsRead(notification.id)
|
||||||
|
loadNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNotificationsPanel = () => {
|
||||||
|
setIsNotificationsPanelOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = { showNotificationsPanel, markNotificationAsRead }
|
||||||
|
|
||||||
|
const value: NotificationsContextType = {
|
||||||
|
notificationEntities,
|
||||||
|
sortedNotifications,
|
||||||
|
unreadNotificationsCount,
|
||||||
|
actions
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNotificationPanelClose = () => {
|
||||||
|
setIsNotificationsPanelOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationsContext.Provider value={value}>
|
||||||
|
{props.children}
|
||||||
|
<ShowIfAuthenticated>
|
||||||
|
<Portal>
|
||||||
|
<NotificationsPanel isOpen={isNotificationsPanelOpen()} onClose={handleNotificationPanelClose} />
|
||||||
|
</Portal>
|
||||||
|
</ShowIfAuthenticated>
|
||||||
|
</NotificationsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// WARNING: need Auth header
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query SignOutQuery {
|
query SignOutQuery {
|
||||||
signOut {
|
signOut {
|
||||||
|
|
9
src/graphql/mutation/mark-notification-as-read.ts
Normal file
9
src/graphql/mutation/mark-notification-as-read.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
mutation MarkNotificationAsReadMutation($notificationId: Int!) {
|
||||||
|
markNotificationAsRead(notification_id: $notificationId) {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -1,5 +1,4 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
|
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||||
unfollow(what: $what, slug: $slug) {
|
unfollow(what: $what, slug: $slug) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
// WARNING: need Auth header
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation ProfileUpdateMutation($profile: ProfileInput!) {
|
mutation ProfileUpdateMutation($profile: ProfileInput!) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export default gql`
|
||||||
error
|
error
|
||||||
token
|
token
|
||||||
user {
|
user {
|
||||||
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
// WARNING: need Auth header
|
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query ShoutsReactedByUserQuery($slug: String!, $limit: Int!, $offset: Int!) {
|
query ShoutsReactedByUserQuery($slug: String!, $limit: Int!, $offset: Int!) {
|
||||||
userReactedShouts(slug: String!, page: Int!, size: Int!) {
|
userReactedShouts(slug: String!, page: Int!, size: Int!) {
|
||||||
|
|
20
src/graphql/query/notifications.ts
Normal file
20
src/graphql/query/notifications.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
query LoadNotificationsQuery {
|
||||||
|
loadNotifications(params: { limit: 10, offset: 0 }) {
|
||||||
|
notifications {
|
||||||
|
id
|
||||||
|
shout
|
||||||
|
reaction
|
||||||
|
type
|
||||||
|
createdAt
|
||||||
|
seen
|
||||||
|
data
|
||||||
|
occurrences
|
||||||
|
}
|
||||||
|
totalCount
|
||||||
|
totalUnreadCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -173,14 +173,15 @@ export type Mutation = {
|
||||||
destroyTopic: Result
|
destroyTopic: Result
|
||||||
follow: Result
|
follow: Result
|
||||||
getSession: AuthResult
|
getSession: AuthResult
|
||||||
|
markAllNotificationsAsRead: Result
|
||||||
markAsRead: Result
|
markAsRead: Result
|
||||||
|
markNotificationAsRead: Result
|
||||||
rateUser: Result
|
rateUser: Result
|
||||||
registerUser: AuthResult
|
registerUser: AuthResult
|
||||||
sendLink: Result
|
sendLink: Result
|
||||||
unfollow: Result
|
unfollow: Result
|
||||||
updateChat: Result
|
updateChat: Result
|
||||||
updateMessage: Result
|
updateMessage: Result
|
||||||
updateOnlineStatus: Result
|
|
||||||
updateProfile: Result
|
updateProfile: Result
|
||||||
updateReaction: Result
|
updateReaction: Result
|
||||||
updateShout: Result
|
updateShout: Result
|
||||||
|
@ -245,6 +246,10 @@ export type MutationMarkAsReadArgs = {
|
||||||
ids: Array<InputMaybe<Scalars['Int']>>
|
ids: Array<InputMaybe<Scalars['Int']>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationMarkNotificationAsReadArgs = {
|
||||||
|
notification_id: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationRateUserArgs = {
|
export type MutationRateUserArgs = {
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
value: Scalars['Int']
|
value: Scalars['Int']
|
||||||
|
@ -296,6 +301,33 @@ export type MutationUpdateTopicArgs = {
|
||||||
input: TopicInput
|
input: TopicInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Notification = {
|
||||||
|
createdAt: Scalars['DateTime']
|
||||||
|
data?: Maybe<Scalars['String']>
|
||||||
|
id: Scalars['Int']
|
||||||
|
occurrences: Scalars['Int']
|
||||||
|
reaction?: Maybe<Scalars['Int']>
|
||||||
|
seen: Scalars['Boolean']
|
||||||
|
shout?: Maybe<Scalars['Int']>
|
||||||
|
type?: Maybe<NotificationType>
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
NewComment = 'NEW_COMMENT',
|
||||||
|
NewReply = 'NEW_REPLY'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationsQueryParams = {
|
||||||
|
limit?: InputMaybe<Scalars['Int']>
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationsQueryResult = {
|
||||||
|
notifications: Array<Maybe<Notification>>
|
||||||
|
totalCount: Scalars['Int']
|
||||||
|
totalUnreadCount: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type Operation = {
|
export type Operation = {
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
name: Scalars['String']
|
name: Scalars['String']
|
||||||
|
@ -324,6 +356,7 @@ export type Query = {
|
||||||
loadChats: Result
|
loadChats: Result
|
||||||
loadDrafts: Array<Maybe<Shout>>
|
loadDrafts: Array<Maybe<Shout>>
|
||||||
loadMessagesBy: Result
|
loadMessagesBy: Result
|
||||||
|
loadNotifications: NotificationsQueryResult
|
||||||
loadReactionsBy: Array<Maybe<Reaction>>
|
loadReactionsBy: Array<Maybe<Reaction>>
|
||||||
loadRecipients: Result
|
loadRecipients: Result
|
||||||
loadShout?: Maybe<Shout>
|
loadShout?: Maybe<Shout>
|
||||||
|
@ -372,6 +405,10 @@ export type QueryLoadMessagesByArgs = {
|
||||||
offset?: InputMaybe<Scalars['Int']>
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryLoadNotificationsArgs = {
|
||||||
|
params: NotificationsQueryParams
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryLoadReactionsByArgs = {
|
export type QueryLoadReactionsByArgs = {
|
||||||
by: ReactionBy
|
by: ReactionBy
|
||||||
limit?: InputMaybe<Scalars['Int']>
|
limit?: InputMaybe<Scalars['Int']>
|
||||||
|
@ -598,12 +635,6 @@ export type Stat = {
|
||||||
viewed?: Maybe<Scalars['Int']>
|
viewed?: Maybe<Scalars['Int']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Subscription = {
|
|
||||||
newMessage?: Maybe<Message>
|
|
||||||
newReaction?: Maybe<Reaction>
|
|
||||||
newShout?: Maybe<Shout>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Token = {
|
export type Token = {
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['DateTime']
|
||||||
expiresAt?: Maybe<Scalars['DateTime']>
|
expiresAt?: Maybe<Scalars['DateTime']>
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { ROUTES } from '../stores/router'
|
|
||||||
import { getServerRoute } from '../utils/getServerRoute'
|
|
||||||
|
|
||||||
export default getServerRoute(ROUTES.authorFollowers)
|
|
|
@ -1,4 +0,0 @@
|
||||||
import { ROUTES } from '../stores/router'
|
|
||||||
import { getServerRoute } from '../utils/getServerRoute'
|
|
||||||
|
|
||||||
export default getServerRoute(ROUTES.authorFollowing)
|
|
|
@ -18,8 +18,6 @@ export const ROUTES = {
|
||||||
author: '/author/:slug',
|
author: '/author/:slug',
|
||||||
authorComments: '/author/:slug/comments',
|
authorComments: '/author/:slug/comments',
|
||||||
authorAbout: '/author/:slug/about',
|
authorAbout: '/author/:slug/about',
|
||||||
authorFollowers: '/author/:slug/followers',
|
|
||||||
authorFollowing: '/author/:slug/following',
|
|
||||||
feed: '/feed',
|
feed: '/feed',
|
||||||
feedMy: '/feed/my',
|
feedMy: '/feed/my',
|
||||||
feedNotifications: '/feed/notifications',
|
feedNotifications: '/feed/notifications',
|
||||||
|
|
|
@ -23,14 +23,6 @@ export type ModalType =
|
||||||
| 'followers'
|
| 'followers'
|
||||||
| 'following'
|
| 'following'
|
||||||
|
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
|
||||||
|
|
||||||
export interface Warning {
|
|
||||||
body: string
|
|
||||||
kind: WarnKind
|
|
||||||
seen?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MODALS: Record<ModalType, ModalType> = {
|
export const MODALS: Record<ModalType, ModalType> = {
|
||||||
auth: 'auth',
|
auth: 'auth',
|
||||||
subscribe: 'subscribe',
|
subscribe: 'subscribe',
|
||||||
|
@ -50,8 +42,6 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
|
|
||||||
const [modal, setModal] = createSignal<ModalType>(null)
|
const [modal, setModal] = createSignal<ModalType>(null)
|
||||||
|
|
||||||
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
|
||||||
|
|
||||||
const { searchParams, changeSearchParam } = useRouter<
|
const { searchParams, changeSearchParam } = useRouter<
|
||||||
AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams
|
AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams
|
||||||
>()
|
>()
|
||||||
|
@ -85,15 +75,6 @@ export const hideModal = () => {
|
||||||
setModal(null)
|
setModal(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const clearWarns = () => setWarnings([])
|
|
||||||
export const warn = (warning: Warning) => setWarnings([...warnings(), warning])
|
|
||||||
|
|
||||||
export const useWarningsStore = () => {
|
|
||||||
return {
|
|
||||||
warnings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useModalStore = () => {
|
export const useModalStore = () => {
|
||||||
return {
|
return {
|
||||||
modal
|
modal
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
--black-400: #696969;
|
--black-400: #696969;
|
||||||
--white-500: #fff;
|
--white-500: #fff;
|
||||||
--blue-500: #2638d9;
|
--blue-500: #2638d9;
|
||||||
|
--yellow-50: #fffbeb;
|
||||||
|
--gray-100: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-editor-dark-mode='true'] {
|
[data-editor-dark-mode='true'] {
|
||||||
|
@ -645,7 +647,7 @@ figure {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,9 @@ import type {
|
||||||
ReactionInput,
|
ReactionInput,
|
||||||
Chat,
|
Chat,
|
||||||
ReactionBy,
|
ReactionBy,
|
||||||
Shout
|
Shout,
|
||||||
|
NotificationsQueryParams,
|
||||||
|
NotificationsQueryResult
|
||||||
} from '../graphql/types.gen'
|
} from '../graphql/types.gen'
|
||||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||||
import { getToken, privateGraphQLClient, privateInboxGraphQLClient } from '../graphql/privateGraphQLClient'
|
import { getToken, privateGraphQLClient, privateInboxGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||||
|
@ -54,6 +56,8 @@ import createMessage from '../graphql/mutation/create-chat-message'
|
||||||
import updateProfile from '../graphql/mutation/update-profile'
|
import updateProfile from '../graphql/mutation/update-profile'
|
||||||
import updateArticle from '../graphql/mutation/article-update'
|
import updateArticle from '../graphql/mutation/article-update'
|
||||||
import deleteShout from '../graphql/mutation/article-delete'
|
import deleteShout from '../graphql/mutation/article-delete'
|
||||||
|
// import notifications from '../graphql/query/notifications'
|
||||||
|
// import markNotificationAsRead from '../graphql/mutation/mark-notification-as-read'
|
||||||
|
|
||||||
type ApiErrorCode =
|
type ApiErrorCode =
|
||||||
| 'unknown'
|
| 'unknown'
|
||||||
|
@ -349,6 +353,21 @@ export const apiClient = {
|
||||||
// console.debug(resp)
|
// console.debug(resp)
|
||||||
return resp.data.loadReactionsBy
|
return resp.data.loadReactionsBy
|
||||||
}
|
}
|
||||||
|
// TODO: store notifications in browser storage
|
||||||
|
/*
|
||||||
|
getNotifications: async (params: NotificationsQueryParams): Promise<NotificationsQueryResult> => {
|
||||||
|
const resp = await privateGraphQLClient.query(notifications, params).toPromise()
|
||||||
|
console.debug(resp.data)
|
||||||
|
return resp.data.loadNotifications
|
||||||
|
},
|
||||||
|
markNotificationAsRead: async (notificationId: number): Promise<void> => {
|
||||||
|
await privateGraphQLClient
|
||||||
|
.mutation(markNotificationAsRead, {
|
||||||
|
notificationId
|
||||||
|
})
|
||||||
|
.toPromise()
|
||||||
|
},
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
export const inboxClient = {
|
export const inboxClient = {
|
||||||
|
|
|
@ -5,7 +5,6 @@ const pageLoadManager: {
|
||||||
export const getPageLoadManagerPromise = () => {
|
export const getPageLoadManagerPromise = () => {
|
||||||
return pageLoadManager.promise
|
return pageLoadManager.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPageLoadManagerPromise = (promise: Promise<any>) => {
|
export const setPageLoadManagerPromise = (promise: Promise<any>) => {
|
||||||
pageLoadManager.promise = promise
|
pageLoadManager.promise = promise
|
||||||
}
|
}
|
||||||
|
|
33
src/utils/sseService.ts
Normal file
33
src/utils/sseService.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
export type EventData = {
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class SSEService {
|
||||||
|
private eventSource: EventSource | null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.eventSource = null
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect(url: string): void {
|
||||||
|
this.eventSource = new EventSource(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect(): void {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close()
|
||||||
|
this.eventSource = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subscribeToEvent(eventName: string, callback: (eventData: EventData) => void): void {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.addEventListener(eventName, (event: MessageEvent) => {
|
||||||
|
const data = JSON.parse(event.data)
|
||||||
|
callback(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SSEService
|
Loading…
Reference in New Issue
Block a user