diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 773d2921..7b5f0ee8 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -211,6 +211,7 @@ "New stories every day and even more!": "New stories and more are waiting for you every day!", "Newsletter": "Newsletter", "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", "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", @@ -244,6 +245,7 @@ "Profile": "Profile", "Profile settings": "Profile settings", "Publications": "Publications", + "PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}", "Publish Album": "Publish Album", "Publish Settings": "Publish Settings", "Punchline": "Punchline", @@ -413,5 +415,6 @@ "video": "video", "view": "view", "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" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index ee6c1e12..ba470f72 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -221,6 +221,7 @@ "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "Newsletter": "Рассылка", "Night mode": "Ночная тема", + "No notifications, yet": "Тут пока пусто", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", @@ -257,6 +258,7 @@ "Profile successfully saved": "Профиль успешно сохранён", "Publication settings": "Настройки публикации", "Publications": "Публикации", + "PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}", "Publish": "Опубликовать", "Publish Album": "Опубликовать альбом", "Publish Settings": "Настройки публикации", @@ -430,14 +432,15 @@ "subscriber": "подписчик", "subscriber_rp": "подписчика", "subscribers": "подписчиков", - "subscription": "подписка", - "subscription_rp": "подписки", - "subscriptions": "подписок", "terms of use": "правилами пользования сайтом", "topics": "темы", "user already exist": "пользователь уже существует", "video": "видео", "view": "просмотр", "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} пользователей}}" } diff --git a/src/components/App.tsx b/src/components/App.tsx index eb5fbf02..f5e054c5 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -40,6 +40,7 @@ import { SnackbarProvider } from '../context/snackbar' import { LocalizeProvider } from '../context/localize' import { ConfirmProvider } from '../context/confirm' import { EditorProvider } from '../context/editor' +import { NotificationsProvider } from '../context/notifications' // TODO: lazy load // const SomePage = lazy(() => import('./Pages/SomePage')) @@ -48,8 +49,6 @@ const pagesMap: Record> = { author: AuthorPage, authorComments: AuthorPage, authorAbout: AuthorPage, - authorFollowing: AuthorPage, - authorFollowers: AuthorPage, inbox: InboxPage, expo: ExpoPage, expoLayout: ExpoPage, @@ -115,9 +114,11 @@ export const App = (props: PageProps) => { - - - + + + + + diff --git a/src/components/Article/AudioHeader/AudioHeader.module.scss b/src/components/Article/AudioHeader/AudioHeader.module.scss index d27a7c60..b07d2c49 100644 --- a/src/components/Article/AudioHeader/AudioHeader.module.scss +++ b/src/components/Article/AudioHeader/AudioHeader.module.scss @@ -5,19 +5,6 @@ .albumInfo { margin-right: 224px; - .topic { - .link { - @include font-size(1.6rem); - - color: var(--blue-link); - border: none; - - &:hover { - text-decoration: underline; - } - } - } - & > h1 { margin: 16px 0 0; } diff --git a/src/components/Article/AudioHeader/AudioHeader.tsx b/src/components/Article/AudioHeader/AudioHeader.tsx index 4569caf3..09f80bd9 100644 --- a/src/components/Article/AudioHeader/AudioHeader.tsx +++ b/src/components/Article/AudioHeader/AudioHeader.tsx @@ -5,8 +5,7 @@ import { MediaItem } from '../../../pages/types' import { createSignal, Show } from 'solid-js' import { Icon } from '../../_shared/Icon' import { Topic } from '../../../graphql/types.gen' -import { getPagePath } from '@nanostores/router' -import { router } from '../../../stores/router' +import { CardTopic } from '../../Feed/CardTopic' type Props = { title: string @@ -29,11 +28,7 @@ export const AudioHeader = (props: Props) => {
- +

{props.title}

diff --git a/src/components/Article/Comment.tsx b/src/components/Article/Comment.tsx index 6cfaf59b..bd4406d8 100644 --- a/src/components/Article/Comment.tsx +++ b/src/components/Article/Comment.tsx @@ -36,8 +36,9 @@ type Props = { export const Comment = (props: Props) => { const { t } = useLocalize() const [isReplyVisible, setIsReplyVisible] = createSignal(false) - const [loading, setLoading] = createSignal(false) - const [editMode, setEditMode] = createSignal(false) + const [loading, setLoading] = createSignal(false) + const [editMode, setEditMode] = createSignal(false) + const [clearEditor, setClearEditor] = createSignal(false) const { session } = useSession() const { @@ -81,11 +82,13 @@ export const Comment = (props: Props) => { body: value, shout: props.comment.shout.id }) + setClearEditor(true) setIsReplyVisible(false) setLoading(false) } catch (error) { console.error('[handleCreate reaction]:', error) } + setClearEditor(false) } const toggleEditMode = () => { @@ -175,11 +178,13 @@ export const Comment = (props: Props) => { handleUpdate(value)} submitByShiftEnter={true} + setClear={clearEditor()} /> diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 874391d6..7a7b616e 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -33,7 +33,7 @@ const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => { } type Props = { - commentAuthors: Author[] + articleAuthors: Author[] shoutSlug: string shoutId: number } @@ -149,7 +149,9 @@ export const CommentsTree = (props: Props) => { {(reaction) => ( a.slug === session()?.user.slug))} + isArticleAuthor={Boolean( + props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug) + )} comment={reaction} lastSeen={dateFromLocalStorage} /> diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index dd9f22a4..7d68604b 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -431,7 +431,7 @@ export const FullArticle = (props: Props) => {
diff --git a/src/components/Author/AuthorCard/AuthorCard.module.scss b/src/components/Author/AuthorCard/AuthorCard.module.scss index db7139f4..240e4be2 100644 --- a/src/components/Author/AuthorCard/AuthorCard.module.scss +++ b/src/components/Author/AuthorCard/AuthorCard.module.scss @@ -90,19 +90,22 @@ .authorName { border: none !important; - display: block; font-size: 1.6rem; font-weight: 500; margin-bottom: 0.8rem; - .listWrapper &:before { - content: ''; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - z-index: 2; + .listWrapper & { + display: block; + + &:before { + content: ''; + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + z-index: 2; + } } } @@ -121,7 +124,45 @@ 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; display: inline-block; height: 24px; @@ -162,251 +203,213 @@ position: absolute; } } - } - a[href*='facebook.com/'] { - &::before { - 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; + &[href*='facebook.com/'] { + &::before { + background-image: url(/icons/user-link-facebook.svg); } - .buttonSubscribedLabel { - display: none; + &:hover { + .authorSubscribeSocialLabel { + display: none; + } } } - .buttonUnfollowLabel { - display: none; + &[href*='twitter.com/'] { + &::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) { 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 { align-items: center; @@ -637,7 +625,8 @@ display: flex; flex-wrap: wrap; font-size: 1.4rem; - margin-top: 0.5rem; + margin-top: 1.5rem; + gap: 1rem; @include media-breakpoint-down(md) { justify-content: center; @@ -648,9 +637,9 @@ align-items: center; cursor: pointer; display: inline-flex; - margin-top: 1rem; margin-right: 3rem; vertical-align: top; + border-bottom: unset !important; &:last-child { margin-right: 0; @@ -679,10 +668,6 @@ .subscribersCounter { font-weight: 500; margin-left: -0.6rem; - - &:hover { - color: #696969; - } } .listWrapper { diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index d0f4989f..2dd2bf8a 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -2,7 +2,7 @@ import type { Author } from '../../../graphql/types.gen' import { Userpic } from '../Userpic' import { Icon } from '../../_shared/Icon' 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 { follow, unfollow } from '../../../stores/zine/common' import { clsx } from 'clsx' @@ -14,12 +14,13 @@ import { openPage, redirectPage } from '@nanostores/router' import { useLocalize } from '../../../context/localize' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { Modal } from '../../Nav/Modal' -import { showModal } from '../../../stores/ui' -import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension' import { SubscriptionFilter } from '../../../pages/types' import { isAuthor } from '../../../utils/isAuthor' import { AuthorBadge } from '../AuthorBadge' 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 = { caption?: string @@ -43,11 +44,11 @@ type Props = { following?: Array showPublicationsCounter?: boolean hideBio?: boolean + isCurrentUser?: boolean } export const AuthorCard = (props: Props) => { const { t, lang } = useLocalize() - const { page } = useRouter() const { session, isSessionLoaded, @@ -117,12 +118,6 @@ export const AuthorCard = (props: Props) => { } }) - createEffect(() => { - if (page().route === 'authorFollowing') { - showModal('following') - } - }) - const handleCloseFollowModals = () => { 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')) { setUserpicUrl(props.author.userpic.replace('100x', '500x500')) } - return ( - <> +
+ + } + > +
+ +
+
+
- - } - > -
- +
+
+ ( + + {children} + + )} + > + {name()} +
- + + {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })} +
+ ) : ( + '' + ) + } + > +
+ -
-
-
- ( - - {children} - - )} - > - {name()} - -
- - {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })} -
- ) : ( - '' - ) - } - > - + + + + +
+ subscribe(false)} + onClick={handleSubscribe} class={clsx('button', styles.button)} classList={{ [styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton, @@ -326,119 +288,152 @@ export const AuthorCard = (props: Props) => { disabled={isSubscribing()} > - + - - {t('Unfollow')} - - - {t('You are subscribed')} + + {t('Follow')} - + } + > + + - - - -
-
+ + + +
- -
+ + + + + <> +

{t('Followers')}

+
+
+
+ + {(follower: Author) => } + +
+
+
+ +
+
+ + +
+ } + /> +
+
+ + + + <> +

{t('Subscriptions')}

+
    +
  • + + {props.following.length} +
  • +
  • + + + {props.following.filter((s) => 'name' in s).length} + +
  • +
  • + + + {props.following.filter((s) => 'title' in s).length} + +
  • +
+
+
+
+
+ + {(subscription) => + isAuthor(subscription) ? ( + + ) : ( + + ) + } + +
+
+
+ +
+
- - - - <> -

{t('Followers')}

-
-
-
- - {(follower: Author) => } - -
-
-
- -
-
- - - - <> -

{t('Subscriptions')}

-
    -
  • - - {props.following.length} -
  • -
  • - - - {props.following.filter((s) => 'name' in s).length} - -
  • -
  • - - - {props.following.filter((s) => 'title' in s).length} - -
  • -
-
-
-
-
- - {(subscription) => - isAuthor(subscription) ? ( - - ) : ( - - ) - } - -
-
-
- -
-
- +
) } diff --git a/src/components/Author/Userpic/Userpic.module.scss b/src/components/Author/Userpic/Userpic.module.scss index 07b0bd89..a37ebfef 100644 --- a/src/components/Author/Userpic/Userpic.module.scss +++ b/src/components/Author/Userpic/Userpic.module.scss @@ -68,7 +68,7 @@ &.big { aspect-ratio: 1/1; - margin: 0 auto; + margin: 0 auto 1rem; max-width: 168px; height: auto; width: 100%; diff --git a/src/components/Discours/Donate.tsx b/src/components/Discours/Donate.tsx index 597be698..08b92b14 100644 --- a/src/components/Discours/Donate.tsx +++ b/src/components/Discours/Donate.tsx @@ -1,7 +1,8 @@ import '../../styles/help.scss' import { createSignal, onMount } from 'solid-js' -import { showModal, warn } from '../../stores/ui' +import { showModal } from '../../stores/ui' import { useLocalize } from '../../context/localize' +import { useSnackbar } from '../../context/snackbar' export const Donate = () => { const { t } = useLocalize() @@ -20,6 +21,9 @@ export const Donate = () => { const [showingPayment, setShowingPayment] = createSignal() const [period, setPeriod] = createSignal(monthly) const [amount, setAmount] = createSignal(0) + const { + actions: { showSnackbar } + } = useSnackbar() const initiated = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -104,10 +108,10 @@ export const Donate = () => { // fail // действие при неуспешной оплате console.debug('[donate] options', options) - warn({ - kind: 'error', - body: reason, - seen: false + + showSnackbar({ + type: 'error', + body: reason }) } ) diff --git a/src/components/Nav/Header/Header.module.scss b/src/components/Nav/Header/Header.module.scss index b678c1bd..9d6fc432 100644 --- a/src/components/Nav/Header/Header.module.scss +++ b/src/components/Nav/Header/Header.module.scss @@ -250,7 +250,7 @@ } .mainNavigationItemActive { - background: var(--link-hover-background); + background: var(--link-hover-background) !important; color: var(--link-hover-color) !important; } diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index a24e2132..5fe286b7 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -3,10 +3,9 @@ import { clsx } from 'clsx' import { router, useRouter } from '../../stores/router' import { Icon } from '../_shared/Icon' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' -import Notifications from './Notifications' import { ProfilePopup } from './ProfilePopup' import { Userpic } from '../Author/Userpic' -import { showModal, useWarningsStore } from '../../stores/ui' +import { showModal } from '../../stores/ui' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { useSession } from '../../context/session' import { useLocalize } from '../../context/localize' @@ -14,6 +13,7 @@ import { getPagePath } from '@nanostores/router' import { Button } from '../_shared/Button' import { useEditorContext } from '../../context/editor' import { Popover } from '../_shared/Popover' +import { useNotifications } from '../../context/notifications' type Props = { setIsProfilePopupVisible: (value: boolean) => void @@ -29,18 +29,17 @@ const MD_WIDTH_BREAKPOINT = 992 export const HeaderAuth = (props: Props) => { const { t } = useLocalize() const { page } = useRouter() - const [visibleWarnings, setVisibleWarnings] = createSignal(false) - const { warnings } = useWarningsStore() - const { session, isSessionLoaded, isAuthenticated } = useSession() + const { + unreadNotificationsCount, + actions: { showNotificationsPanel } + } = useNotifications() const { form, actions: { toggleEditorPanel, saveShout, publishShout } } = useEditorContext() - const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) - const handleBellIconClick = (event: Event) => { event.preventDefault() @@ -48,15 +47,16 @@ export const HeaderAuth = (props: Props) => { showModal('auth') return } - toggleWarnings() + + showNotificationsPanel() } const isEditorPage = createMemo(() => page().route === 'edit' || page().route === 'editSettings') - const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage()) - const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage()) - const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage()) - const showAuthenticatedControls = createMemo(() => isAuthenticated()) + const isNotificationsVisible = createMemo(() => isAuthenticated() && !isEditorPage()) + const isSaveButtonVisible = createMemo(() => isAuthenticated() && isEditorPage()) + const isCreatePostButtonVisible = createMemo(() => isAuthenticated() && !isEditorPage()) + const isAuthenticatedControlsVisible = createMemo(() => isAuthenticated()) const handleBurgerButtonClick = () => { toggleEditorPanel() @@ -71,8 +71,9 @@ export const HeaderAuth = (props: Props) => { } const [width, setWidth] = createSignal(0) - const handleResize = () => setWidth(window.innerWidth) + onMount(() => { + const handleResize = () => setWidth(window.innerWidth) handleResize() window.addEventListener('resize', handleResize) onCleanup(() => window.removeEventListener('resize', handleResize)) @@ -109,7 +110,7 @@ export const HeaderAuth = (props: Props) => {
diff --git a/src/components/Nav/Notifications.tsx b/src/components/Nav/Notifications.tsx deleted file mode 100644 index 4a53736d..00000000 --- a/src/components/Nav/Notifications.tsx +++ /dev/null @@ -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 ( - 0}> - -
    - {(warning) =>
  • {warning.body}
  • }
    -
-
-
- ) -} diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index c6d7ec35..6e6efba5 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -26,7 +26,7 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
{t('Drafts')}
  • - + {t('Subscriptions')}
  • diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss new file mode 100644 index 00000000..284af187 --- /dev/null +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss @@ -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; +} diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx new file mode 100644 index 00000000..397572fb --- /dev/null +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx @@ -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(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 ( + +
    + +
    {content()}
    +
    + {/*{formatDate(new Date(props.notification.createdAt), { month: 'numeric' })}*/} +
    +
    +
    + ) +} diff --git a/src/components/NotificationsPanel/NotificationView/index.ts b/src/components/NotificationsPanel/NotificationView/index.ts new file mode 100644 index 00000000..59dcafc6 --- /dev/null +++ b/src/components/NotificationsPanel/NotificationView/index.ts @@ -0,0 +1 @@ +export { NotificationView } from './NotificationView' diff --git a/src/components/NotificationsPanel/NotificationsPanel.module.scss b/src/components/NotificationsPanel/NotificationsPanel.module.scss new file mode 100644 index 00000000..a1b18dd0 --- /dev/null +++ b/src/components/NotificationsPanel/NotificationsPanel.module.scss @@ -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; +} diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx new file mode 100644 index 00000000..ddfc66b2 --- /dev/null +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -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 ( +
    +
    (panelRef.current = el)} class={styles.panel}> +
    + {/*TODO: check markup (hover)*/} + +
    +
    {t('Notifications')}
    + {t('No notifications, yet')}
    } + > + {(notification) => ( + + )} + +
    +
    + ) +} diff --git a/src/components/NotificationsPanel/index.ts b/src/components/NotificationsPanel/index.ts new file mode 100644 index 00000000..5d134561 --- /dev/null +++ b/src/components/NotificationsPanel/index.ts @@ -0,0 +1 @@ +export { NotificationsPanel } from './NotificationsPanel' diff --git a/src/components/Topic/Card.module.scss b/src/components/Topic/Card.module.scss index e2f02fbf..c5b56fbb 100644 --- a/src/components/Topic/Card.module.scss +++ b/src/components/Topic/Card.module.scss @@ -60,7 +60,7 @@ margin-bottom: 1.2rem; margin-top: 0.5rem !important; - a { + a:link { border: none; } } diff --git a/src/components/Views/Author/Author.module.scss b/src/components/Views/Author/Author.module.scss index 45f849a3..e7da012c 100644 --- a/src/components/Views/Author/Author.module.scss +++ b/src/components/Views/Author/Author.module.scss @@ -14,10 +14,14 @@ } .authorHeader { - border-bottom: 2px solid #000; + border-bottom: 2px solid var(--default-color); margin-bottom: 2.4rem; - margin-top: -3.2rem; + margin-top: 1.8rem; padding-bottom: 4rem; + + @include media-breakpoint-up(lg) { + margin-top: -3.2rem; + } } .ratingContainer { diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index b0e5e161..027d260d 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -18,6 +18,7 @@ import { useLocalize } from '../../../context/localize' import { AuthorRatingControl } from '../../Author/AuthorRatingControl' import { hideModal } from '../../../stores/ui' import { getPagePath } from '@nanostores/router' +import { useSession } from '../../../context/session' type Props = { shouts: Shout[] @@ -33,7 +34,8 @@ export const AuthorView = (props: Props) => { const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { authorEntities } = useAuthorsStore({ authors: [props.author] }) - const { page } = useRouter() + const { page: getPage } = useRouter() + const { user } = useSession() const author = createMemo(() => authorEntities()[props.authorSlug]) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false) @@ -102,14 +104,14 @@ export const AuthorView = (props: Props) => { // return t('Top recent') // }) - const shouts = createMemo(() => + const pages = createMemo(() => splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) const [commented, setCommented] = createSignal([]) createEffect(async () => { - if (page().route === 'authorComments') { + if (getPage().route === 'authorComments') { try { const data = await apiClient.getReactionsBy({ by: { comment: true, createdBy: props.authorSlug } @@ -131,23 +133,24 @@ export const AuthorView = (props: Props) => { isAuthorPage={true} followers={followers()} following={following()} + isCurrentUser={author().slug === user()?.slug} />
    - +
    @@ -191,7 +194,7 @@ export const AuthorView = (props: Props) => {
    - +
    @@ -204,8 +207,7 @@ export const AuthorView = (props: Props) => {
    - - + @@ -226,15 +228,15 @@ export const AuthorView = (props: Props) => { - - {(shout) => ( + + {(page) => ( <> - - - - - - + + + + + + )} diff --git a/src/components/_shared/PageLayout.module.scss b/src/components/_shared/PageLayout.module.scss index e3fb1682..29e1207f 100644 --- a/src/components/_shared/PageLayout.module.scss +++ b/src/components/_shared/PageLayout.module.scss @@ -1,4 +1,4 @@ -.withPadding { +main.withPadding { padding-top: 58px; } diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index be4e29f6..cc28b618 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -31,7 +31,7 @@ export const PageLayout = (props: Props) => { props.scrollToComments(scrollToComments()) } }) - // const { randomTopics } = useTopicsStore() + return ( <> diff --git a/src/components/_shared/Popup/Popup.module.scss b/src/components/_shared/Popup/Popup.module.scss index ff25c424..d16a0e05 100644 --- a/src/components/_shared/Popup/Popup.module.scss +++ b/src/components/_shared/Popup/Popup.module.scss @@ -73,7 +73,8 @@ padding-top: 1em; } - a { + a:link, + :global(.link) { border: none; white-space: nowrap; @@ -100,7 +101,9 @@ .shareControl { text-align: left; - transition: color 0.3s, background-color 0.3s; + transition: + color 0.3s, + background-color 0.3s; white-space: nowrap; &:hover { diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx new file mode 100644 index 00000000..06802100 --- /dev/null +++ b/src/context/notifications.tsx @@ -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 + unreadNotificationsCount: Accessor + sortedNotifications: Accessor + actions: { + showNotificationsPanel: () => void + markNotificationAsRead: (notification: Notification) => Promise + } +} + +const NotificationsContext = createContext() + +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>({}) + + 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 ( + + {props.children} + + + + + + + ) +} diff --git a/src/graphql/mutation/auth-logout.ts b/src/graphql/mutation/auth-logout.ts index be6ea2a1..8ffa91c9 100644 --- a/src/graphql/mutation/auth-logout.ts +++ b/src/graphql/mutation/auth-logout.ts @@ -1,7 +1,5 @@ import { gql } from '@urql/core' -// WARNING: need Auth header - export default gql` query SignOutQuery { signOut { diff --git a/src/graphql/mutation/mark-notification-as-read.ts b/src/graphql/mutation/mark-notification-as-read.ts new file mode 100644 index 00000000..8b7b1e53 --- /dev/null +++ b/src/graphql/mutation/mark-notification-as-read.ts @@ -0,0 +1,9 @@ +import { gql } from '@urql/core' + +export default gql` + mutation MarkNotificationAsReadMutation($notificationId: Int!) { + markNotificationAsRead(notification_id: $notificationId) { + error + } + } +` diff --git a/src/graphql/mutation/unfollow.ts b/src/graphql/mutation/unfollow.ts index 9b2d8198..763b49d7 100644 --- a/src/graphql/mutation/unfollow.ts +++ b/src/graphql/mutation/unfollow.ts @@ -1,5 +1,4 @@ import { gql } from '@urql/core' - export default gql` mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) { unfollow(what: $what, slug: $slug) { diff --git a/src/graphql/mutation/update-profile.ts b/src/graphql/mutation/update-profile.ts index ca6c12ea..25de9830 100644 --- a/src/graphql/mutation/update-profile.ts +++ b/src/graphql/mutation/update-profile.ts @@ -1,5 +1,4 @@ import { gql } from '@urql/core' -// WARNING: need Auth header export default gql` mutation ProfileUpdateMutation($profile: ProfileInput!) { diff --git a/src/graphql/query/auth-login.ts b/src/graphql/query/auth-login.ts index 7dcafc83..ee233d0c 100644 --- a/src/graphql/query/auth-login.ts +++ b/src/graphql/query/auth-login.ts @@ -6,6 +6,7 @@ export default gql` error token user { + id name slug userpic diff --git a/src/graphql/query/author-reacted-shouts.ts b/src/graphql/query/author-reacted-shouts.ts index 780e9c3a..1ab9e2ad 100644 --- a/src/graphql/query/author-reacted-shouts.ts +++ b/src/graphql/query/author-reacted-shouts.ts @@ -1,7 +1,5 @@ import { gql } from '@urql/core' -// WARNING: need Auth header - export default gql` query ShoutsReactedByUserQuery($slug: String!, $limit: Int!, $offset: Int!) { userReactedShouts(slug: String!, page: Int!, size: Int!) { diff --git a/src/graphql/query/notifications.ts b/src/graphql/query/notifications.ts new file mode 100644 index 00000000..a1951b0f --- /dev/null +++ b/src/graphql/query/notifications.ts @@ -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 + } + } +` diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 0aa3f6b8..40384814 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -173,14 +173,15 @@ export type Mutation = { destroyTopic: Result follow: Result getSession: AuthResult + markAllNotificationsAsRead: Result markAsRead: Result + markNotificationAsRead: Result rateUser: Result registerUser: AuthResult sendLink: Result unfollow: Result updateChat: Result updateMessage: Result - updateOnlineStatus: Result updateProfile: Result updateReaction: Result updateShout: Result @@ -245,6 +246,10 @@ export type MutationMarkAsReadArgs = { ids: Array> } +export type MutationMarkNotificationAsReadArgs = { + notification_id: Scalars['Int'] +} + export type MutationRateUserArgs = { slug: Scalars['String'] value: Scalars['Int'] @@ -296,6 +301,33 @@ export type MutationUpdateTopicArgs = { input: TopicInput } +export type Notification = { + createdAt: Scalars['DateTime'] + data?: Maybe + id: Scalars['Int'] + occurrences: Scalars['Int'] + reaction?: Maybe + seen: Scalars['Boolean'] + shout?: Maybe + type?: Maybe +} + +export enum NotificationType { + NewComment = 'NEW_COMMENT', + NewReply = 'NEW_REPLY' +} + +export type NotificationsQueryParams = { + limit?: InputMaybe + offset?: InputMaybe +} + +export type NotificationsQueryResult = { + notifications: Array> + totalCount: Scalars['Int'] + totalUnreadCount: Scalars['Int'] +} + export type Operation = { id: Scalars['Int'] name: Scalars['String'] @@ -324,6 +356,7 @@ export type Query = { loadChats: Result loadDrafts: Array> loadMessagesBy: Result + loadNotifications: NotificationsQueryResult loadReactionsBy: Array> loadRecipients: Result loadShout?: Maybe @@ -372,6 +405,10 @@ export type QueryLoadMessagesByArgs = { offset?: InputMaybe } +export type QueryLoadNotificationsArgs = { + params: NotificationsQueryParams +} + export type QueryLoadReactionsByArgs = { by: ReactionBy limit?: InputMaybe @@ -598,12 +635,6 @@ export type Stat = { viewed?: Maybe } -export type Subscription = { - newMessage?: Maybe - newReaction?: Maybe - newShout?: Maybe -} - export type Token = { createdAt: Scalars['DateTime'] expiresAt?: Maybe diff --git a/src/pages/authorFollowers.page.route.ts b/src/pages/authorFollowers.page.route.ts deleted file mode 100644 index c18a617a..00000000 --- a/src/pages/authorFollowers.page.route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ROUTES } from '../stores/router' -import { getServerRoute } from '../utils/getServerRoute' - -export default getServerRoute(ROUTES.authorFollowers) diff --git a/src/pages/authorFollowing.page.route.ts b/src/pages/authorFollowing.page.route.ts deleted file mode 100644 index a8925f2f..00000000 --- a/src/pages/authorFollowing.page.route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ROUTES } from '../stores/router' -import { getServerRoute } from '../utils/getServerRoute' - -export default getServerRoute(ROUTES.authorFollowing) diff --git a/src/stores/router.ts b/src/stores/router.ts index 316c4f1c..01755a1e 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -18,8 +18,6 @@ export const ROUTES = { author: '/author/:slug', authorComments: '/author/:slug/comments', authorAbout: '/author/:slug/about', - authorFollowers: '/author/:slug/followers', - authorFollowing: '/author/:slug/following', feed: '/feed', feedMy: '/feed/my', feedNotifications: '/feed/notifications', diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 4bcdba76..b9600651 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -23,14 +23,6 @@ export type ModalType = | 'followers' | 'following' -type WarnKind = 'error' | 'warn' | 'info' - -export interface Warning { - body: string - kind: WarnKind - seen?: boolean -} - export const MODALS: Record = { auth: 'auth', subscribe: 'subscribe', @@ -50,8 +42,6 @@ export const MODALS: Record = { const [modal, setModal] = createSignal(null) -const [warnings, setWarnings] = createSignal([]) - const { searchParams, changeSearchParam } = useRouter< AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams >() @@ -85,15 +75,6 @@ export const hideModal = () => { setModal(null) } -export const clearWarns = () => setWarnings([]) -export const warn = (warning: Warning) => setWarnings([...warnings(), warning]) - -export const useWarningsStore = () => { - return { - warnings - } -} - export const useModalStore = () => { return { modal diff --git a/src/styles/app.scss b/src/styles/app.scss index 9c360100..d2731f41 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -36,6 +36,8 @@ --black-400: #696969; --white-500: #fff; --blue-500: #2638d9; + --yellow-50: #fffbeb; + --gray-100: #f3f4f6; } [data-editor-dark-mode='true'] { @@ -645,7 +647,7 @@ figure { cursor: default; &:hover { - color: #fff; + background: #fff; } } } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 2eee2cb2..5ab7e9e4 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -15,7 +15,9 @@ import type { ReactionInput, Chat, ReactionBy, - Shout + Shout, + NotificationsQueryParams, + NotificationsQueryResult } from '../graphql/types.gen' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' 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 updateArticle from '../graphql/mutation/article-update' import deleteShout from '../graphql/mutation/article-delete' +// import notifications from '../graphql/query/notifications' +// import markNotificationAsRead from '../graphql/mutation/mark-notification-as-read' type ApiErrorCode = | 'unknown' @@ -349,6 +353,21 @@ export const apiClient = { // console.debug(resp) return resp.data.loadReactionsBy } + // TODO: store notifications in browser storage + /* + getNotifications: async (params: NotificationsQueryParams): Promise => { + const resp = await privateGraphQLClient.query(notifications, params).toPromise() + console.debug(resp.data) + return resp.data.loadNotifications + }, + markNotificationAsRead: async (notificationId: number): Promise => { + await privateGraphQLClient + .mutation(markNotificationAsRead, { + notificationId + }) + .toPromise() + }, + */ } export const inboxClient = { diff --git a/src/utils/pageLoadManager.ts b/src/utils/pageLoadManager.ts index d04a3d3d..a25ff347 100644 --- a/src/utils/pageLoadManager.ts +++ b/src/utils/pageLoadManager.ts @@ -5,7 +5,6 @@ const pageLoadManager: { export const getPageLoadManagerPromise = () => { return pageLoadManager.promise } - export const setPageLoadManagerPromise = (promise: Promise) => { pageLoadManager.promise = promise } diff --git a/src/utils/sseService.ts b/src/utils/sseService.ts new file mode 100644 index 00000000..20121c35 --- /dev/null +++ b/src/utils/sseService.ts @@ -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