From 85e8533931b33ee55537ea2f2dc0db81b5cfb2fa Mon Sep 17 00:00:00 2001 From: Kosta <47947996+dobrodob@users.noreply.github.com> Date: Wed, 18 Oct 2023 13:56:41 +0300 Subject: [PATCH] dates in notifications, lots of minor fixes (#271) * dates in notifications, lots of minor fixes Co-authored-by: Igor Lobanov --- package-lock.json | 31 +++++ package.json | 1 + public/locales/en/translation.json | 25 ++-- public/locales/ru/translation.json | 25 ++-- src/components/Article/CommentDate.tsx | 5 +- src/components/Article/FullArticle.tsx | 7 +- .../Author/AuthorBadge/AuthorBadge.tsx | 3 +- .../Author/AuthorCard/AuthorCard.tsx | 5 +- src/components/Draft/Draft.tsx | 8 +- src/components/Feed/ArticleCard.tsx | 13 +- src/components/Inbox/DialogCard.tsx | 5 +- src/components/Inbox/Message.tsx | 5 +- .../NotificationView.module.scss | 5 + .../NotificationView/NotificationView.tsx | 24 +++- .../NotificationsPanel.module.scss | 9 ++ .../NotificationsPanel/NotificationsPanel.tsx | 88 ++++++++++++-- src/components/Topic/Card.tsx | 13 +- .../Views/AllAuthors.module.scss} | 8 +- src/components/Views/AllAuthors.tsx | 5 +- src/components/Views/AllTopics.module.scss | 115 ++++++++++++++++++ src/components/Views/AllTopics.tsx | 16 ++- src/components/_shared/Stat.module.scss | 36 ------ src/components/_shared/StatMetrics.tsx | 45 ------- .../_shared/TimeAgo/TimeAgo.module.scss | 3 + src/components/_shared/TimeAgo/TimeAgo.tsx | 37 ++++++ src/components/_shared/TimeAgo/index.ts | 1 + src/context/localize.tsx | 53 +++++++- src/renderer/_default.page.server.tsx | 7 +- src/utils/apiClient.ts | 2 +- src/utils/capitalize.ts | 9 ++ src/utils/dummyFilter.ts | 17 ++- src/utils/formatDateTime.ts | 17 --- src/utils/index.ts | 83 ------------- 33 files changed, 450 insertions(+), 276 deletions(-) rename src/{styles/AllTopics.module.scss => components/Views/AllAuthors.module.scss} (95%) create mode 100644 src/components/Views/AllTopics.module.scss delete mode 100644 src/components/_shared/Stat.module.scss delete mode 100644 src/components/_shared/StatMetrics.tsx create mode 100644 src/components/_shared/TimeAgo/TimeAgo.module.scss create mode 100644 src/components/_shared/TimeAgo/TimeAgo.tsx create mode 100644 src/components/_shared/TimeAgo/index.ts create mode 100644 src/utils/capitalize.ts delete mode 100644 src/utils/formatDateTime.ts delete mode 100644 src/utils/index.ts diff --git a/package-lock.json b/package-lock.json index 4553ab3a..d30250d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "husky": "8.0.3", "hygen": "6.2.11", "i18next-http-backend": "2.2.0", + "javascript-time-ago": "2.5.9", "jest": "29.7.0", "js-cookie": "3.0.5", "lint-staged": "14.0.1", @@ -11426,6 +11427,15 @@ "node": ">=8" } }, + "node_modules/javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "dev": true, + "dependencies": { + "relative-time-format": "^1.1.6" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -16237,6 +16247,12 @@ "jsesc": "bin/jsesc" } }, + "node_modules/relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==", + "dev": true + }, "node_modules/relay-runtime": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", @@ -26822,6 +26838,15 @@ } } }, + "javascript-time-ago": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/javascript-time-ago/-/javascript-time-ago-2.5.9.tgz", + "integrity": "sha512-pQ8mNco/9g9TqWXWWjP0EWl6i/lAQScOyEeXy5AB+f7MfLSdgyV9BJhiOD1zrIac/lrxPYOWNbyl/IW8CW5n0A==", + "dev": true, + "requires": { + "relative-time-format": "^1.1.6" + } + }, "jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -30313,6 +30338,12 @@ } } }, + "relative-time-format": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/relative-time-format/-/relative-time-format-1.1.6.tgz", + "integrity": "sha512-aCv3juQw4hT1/P/OrVltKWLlp15eW1GRcwP1XdxHrPdZE9MtgqFpegjnTjLhi2m2WI9MT/hQQtE+tjEWG1hgkQ==", + "dev": true + }, "relay-runtime": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", diff --git a/package.json b/package.json index 7de87825..789d8af9 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "husky": "8.0.3", "hygen": "6.2.11", "i18next-http-backend": "2.2.0", + "javascript-time-ago": "2.5.9", "jest": "29.7.0", "js-cookie": "3.0.5", "lint-staged": "14.0.1", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 577ddfcf..cad81887 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -210,21 +210,17 @@ "New only": "New only", "New password": "New password", "New stories every day and even more!": "New stories and more are waiting for you every day!", - - "NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication", - "NotificationNewCommentText2": "from", - "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}", - - "NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication", - "NotificationNewReplyText2": "from", - "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}", - "Newsletter": "Newsletter", "Night mode": "Night mode", "No notifications yet": "No notifications yet", - "Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here", "Nothing here yet": "There's nothing here yet", "Nothing is here": "There is nothing here", + "NotificationNewCommentText1": "{commentsCount, plural, one {New comment} other {{commentsCount} comments}} to your publication", + "NotificationNewCommentText2": "from", + "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { one more user} other { and more {restUsersCount} users}}", + "NotificationNewReplyText1": "{commentsCount, plural, one {New reply} other {{commentsCount} replays}} to your publication", + "NotificationNewReplyText2": "from", + "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { and one more user} other { and more {restUsersCount} users}}", "Notifications": "Notifications", "Or paste a link to an image": "Or paste a link to an image", "Ordered list": "Ordered list", @@ -369,6 +365,7 @@ "Write about the topic": "Write about the topic", "Write an article": "Write an article", "Write comment": "Write comment", + "Write good articles, comment\nand it won't be so empty here": "Write good articles, comment\nand it won't be so empty here", "Write message": "Write a message", "Write to us": "Write to us", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "You can download multiple tracks at once in .mp3, .wav or .flac formats", @@ -385,6 +382,7 @@ "article": "article", "author": "author", "authors": "authors", + "authorsWithCount": "{count} {count, plural, one {author} other {authors}}", "back to menu": "back to menu", "bold": "bold", "bookmarks": "bookmarks", @@ -395,10 +393,12 @@ "delimiter": "delimiter", "discussion": "discourse", "drafts": "drafts", + "earlier": "earlier", "email not confirmed": "email not confirmed", "enter": "enter", "feed": "feed", "follower": "follower", + "followersWithCount": "{count} {count, plural, one {follower} other {followers}}", "general feed": "general tape", "header 1": "header 1", "header 2": "header 2", @@ -420,6 +420,7 @@ "register": "register", "repeat": "repeat", "shout": "post", + "shoutsWithCount": "{count} {count, plural, one {post} other {posts}}", "sign up or sign in": "sign up or sign in", "slug is used by another user": "Slug is already taken by another user", "subscriber": "subscriber", @@ -429,8 +430,10 @@ "subscription_rp": "subscription", "subscriptions": "subscriptions", "terms of use": "terms of use", + "today": "today", "topics": "topics", "user already exist": "user already exists", "video": "video", - "view": "view" + "view": "view", + "yesterday": "yesterday" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index b8f5d551..07af69c1 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -220,22 +220,18 @@ "New only": "Только новые", "New password": "Новый пароль", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", - - "NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации", - "NotificationNewCommentText2": "от", - "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", - - "NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации", - "NotificationNewReplyText2": "от", - "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", - "Newsletter": "Рассылка", "Night mode": "Ночная тема", "No notifications yet": "Уведомлений пока нет", - "Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "Nothing here yet": "Здесь пока ничего нет", "Nothing is here": "Здесь ничего нет", + "NotificationNewCommentText1": "{commentsCount, plural, one {Новый комментарий} few {{commentsCount} новых комментария} other {{commentsCount} новых комментариев}} к вашей публикации", + "NotificationNewCommentText2": "от", + "NotificationNewCommentText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", + "NotificationNewReplyText1": "{commentsCount, plural, one {Новый ответ} few {{commentsCount} новых ответа} other {{commentsCount} новых ответов}} на ваш комментарий к публикации", + "NotificationNewReplyText2": "от", + "NotificationNewReplyText3": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", "Notifications": "Уведомления", "Or paste a link to an image": "Или вставьте ссылку на изображение", "Ordered list": "Нумерованный список", @@ -389,6 +385,7 @@ "Write about the topic": "Написать в тему", "Write an article": "Написать статью", "Write comment": "Написать комментарий", + "Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто", "Write message": "Написать сообщение", "Write to us": "Напишите нам", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac", @@ -405,6 +402,7 @@ "article": "статья", "author": "автор", "authors": "авторы", + "authorsWithCount": "{count} {count, plural, one {автор} few {автора} other {авторов}}", "back to menu": "назад в меню", "bold": "жирный", "bookmarks": "закладки", @@ -418,10 +416,12 @@ "discourse_theme": "Тема дискурса", "discussion": "дискурс", "drafts": "черновики", + "earlier": "ранее", "email not confirmed": "email не подтвержден", "enter": "войдите", "feed": "лента", "follower": "подписчик", + "followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}", "general feed": "Общая лента", "header 1": "заголовок 1", "header 2": "заголовок 2", @@ -444,6 +444,7 @@ "register": "зарегистрируйтесь", "repeat": "повторить", "shout": "пост", + "shoutsWithCount": "{count} {count, plural, one {пост} few {поста} other {постов}}", "sign in": "войти", "sign up": "зарегистрироваться", "sign up or sign in": "зарегистрироваться или войти", @@ -453,8 +454,10 @@ "subscriber_rp": "подписчика", "subscribers": "подписчиков", "terms of use": "правилами пользования сайтом", + "today": "сегодня", "topics": "темы", "user already exist": "пользователь уже существует", "video": "видео", - "view": "просмотр" + "view": "просмотр", + "yesterday": "вчера" } diff --git a/src/components/Article/CommentDate.tsx b/src/components/Article/CommentDate.tsx index 28500a9d..1d26de95 100644 --- a/src/components/Article/CommentDate.tsx +++ b/src/components/Article/CommentDate.tsx @@ -1,7 +1,6 @@ import { Show } from 'solid-js' import { Icon } from '../_shared/Icon' import type { Reaction } from '../../graphql/types.gen' -import { formatDate } from '../../utils' import { useLocalize } from '../../context/localize' import { clsx } from 'clsx' import styles from './CommentDate.module.scss' @@ -13,9 +12,9 @@ type Props = { } export const CommentDate = (props: Props) => { - const { t } = useLocalize() + const { t, formatDate } = useLocalize() - const formattedDate = (date) => { + const formattedDate = (date: number) => { const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort ? { month: 'long', day: 'numeric', year: 'numeric' } : { hour: 'numeric', minute: 'numeric' } diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 7647b2b1..3f26f74f 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -8,8 +8,7 @@ import { useSession } from '../../context/session' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' import { MediaItem } from '../../pages/types' -import { router, useRouter } from '../../stores/router' -import { formatDate } from '../../utils' +import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { getDescription } from '../../utils/meta' import { imageProxy } from '../../utils/imageProxy' import { AuthorCard } from '../Author/AuthorCard' @@ -42,14 +41,14 @@ const scrollTo = (el: HTMLElement) => { const { top } = el.getBoundingClientRect() window.scrollTo({ - top: top + window.scrollY - 96, + top: top + window.scrollY - DEFAULT_HEADER_OFFSET, left: 0, behavior: 'smooth' }) } export const FullArticle = (props: Props) => { - const { t } = useLocalize() + const { t, formatDate } = useLocalize() const { user, isAuthenticated, diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 772b4479..e8730fb3 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -3,7 +3,6 @@ import styles from './AuthorBadge.module.scss' import { Userpic } from '../Userpic' import { Author, FollowingEntity } from '../../../graphql/types.gen' import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' -import { formatDate } from '../../../utils' import { useLocalize } from '../../../context/localize' import { Button } from '../../_shared/Button' import { useSession } from '../../../context/session' @@ -21,7 +20,7 @@ export const AuthorBadge = (props: Props) => { actions: { loadSession, requireAuthentication } } = useSession() - const { t } = useLocalize() + const { t, formatDate } = useLocalize() const subscribed = createMemo(() => { return session()?.news?.authors?.some((u) => u === props.author.slug) || false }) diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 75e6b2f2..9ac66a04 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -1,8 +1,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, Match, Show, Switch } from 'solid-js' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { translit } from '../../../utils/ru2en' import { follow, unfollow } from '../../../stores/zine/common' import { clsx } from 'clsx' @@ -20,7 +19,7 @@ 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' +import styles from './AuthorCard.module.scss' type Props = { caption?: string diff --git a/src/components/Draft/Draft.tsx b/src/components/Draft/Draft.tsx index 7ee25154..32d6edcb 100644 --- a/src/components/Draft/Draft.tsx +++ b/src/components/Draft/Draft.tsx @@ -2,8 +2,6 @@ import { clsx } from 'clsx' import styles from './Draft.module.scss' import type { Shout } from '../../graphql/types.gen' import { Icon } from '../_shared/Icon' -import { formatDate } from '../../utils' -import formatDateTime from '../../utils/formatDateTime' import { useLocalize } from '../../context/localize' import { useConfirm } from '../../context/confirm' import { useSnackbar } from '../../context/snackbar' @@ -18,7 +16,7 @@ type Props = { } export const Draft = (props: Props) => { - const { t } = useLocalize() + const { t, formatDate } = useLocalize() const { actions: { showConfirm } } = useConfirm() @@ -51,8 +49,8 @@ export const Draft = (props: Props) => { return (
- {formatDate(new Date(props.shout.createdAt))} -  {formatDateTime(props.shout.createdAt)()} + {' '} + {formatDate(new Date(props.shout.createdAt), { hour: '2-digit', minute: '2-digit' })}
{props.shout.title || t('Unnamed draft')} {props.shout.subtitle} diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx index 4ddd6551..6799fa03 100644 --- a/src/components/Feed/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard.tsx @@ -1,6 +1,5 @@ import { createMemo, createSignal, For, Show } from 'solid-js' import type { Shout } from '../../graphql/types.gen' -import { capitalize, formatDate } from '../../utils' import { Icon } from '../_shared/Icon' import styles from './ArticleCard.module.scss' import { clsx } from 'clsx' @@ -17,6 +16,7 @@ import { imageProxy } from '../../utils/imageProxy' import { Popover } from '../_shared/Popover' import { AuthorCard } from '../Author/AuthorCard' import { useSession } from '../../context/session' +import { capitalize } from '../../utils/capitalize' interface ArticleCardProps { settings?: { @@ -44,7 +44,12 @@ interface ArticleCardProps { article: Shout } -const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => { +const getTitleAndSubtitle = ( + article: Shout +): { + title: string + subtitle: string +} => { let title = article.title let subtitle = article.subtitle @@ -66,14 +71,14 @@ const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } export const ArticleCard = (props: ArticleCardProps) => { - const { t, lang } = useLocalize() + const { t, lang, formatDate } = useLocalize() const { user } = useSession() const mainTopic = props.article.topics.find((articleTopic) => articleTopic.slug === props.article.mainTopic) || props.article.topics[0] const formattedDate = createMemo(() => { - return formatDate(new Date(props.article.createdAt), { month: 'long', day: 'numeric', year: 'numeric' }) + return formatDate(new Date(props.article.createdAt)) }) const { title, subtitle } = getTitleAndSubtitle(props.article) diff --git a/src/components/Inbox/DialogCard.tsx b/src/components/Inbox/DialogCard.tsx index 0e5021de..4a671ab0 100644 --- a/src/components/Inbox/DialogCard.tsx +++ b/src/components/Inbox/DialogCard.tsx @@ -2,7 +2,6 @@ import { Show, Switch, Match, createMemo } from 'solid-js' import DialogAvatar from './DialogAvatar' import type { ChatMember } from '../../graphql/types.gen' import GroupDialogAvatar from './GroupDialogAvatar' -import formattedTime from '../../utils/formatDateTime' import { clsx } from 'clsx' import styles from './DialogCard.module.scss' import { useLocalize } from '../../context/localize' @@ -20,7 +19,7 @@ type DialogProps = { } const DialogCard = (props: DialogProps) => { - const { t } = useLocalize() + const { t, formatTime } = useLocalize() const companions = createMemo( () => props.members && props.members.filter((member) => member.id !== props.ownId) ) @@ -64,7 +63,7 @@ const DialogCard = (props: DialogProps) => {
-
{formattedTime(props.lastUpdate * 1000)()}
+
{formatTime(new Date(props.lastUpdate * 1000))}
0}>
diff --git a/src/components/Inbox/Message.tsx b/src/components/Inbox/Message.tsx index 9a345cf4..c9a9df00 100644 --- a/src/components/Inbox/Message.tsx +++ b/src/components/Inbox/Message.tsx @@ -3,10 +3,10 @@ import { clsx } from 'clsx' import styles from './Message.module.scss' import DialogAvatar from './DialogAvatar' import type { Message as MessageType, ChatMember } from '../../graphql/types.gen' -import formattedTime from '../../utils/formatDateTime' import { Icon } from '../_shared/Icon' import { MessageActionsPopup } from './MessageActionsPopup' import QuotedMessage from './QuotedMessage' +import { useLocalize } from '../../context/localize' type Props = { content: MessageType @@ -18,6 +18,7 @@ type Props = { } export const Message = (props: Props) => { + const { formatTime } = useLocalize() const isOwn = props.ownId === Number(props.content.author) const user = props.members?.find((m) => m.id === Number(props.content.author)) const [isPopupVisible, setIsPopupVisible] = createSignal(false) @@ -47,7 +48,7 @@ export const Message = (props: Props) => {
-
{formattedTime(props.content.createdAt * 1000)()}
+
{formatTime(new Date(props.content.createdAt * 1000))}
) } diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss index 26401acd..b81dab01 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.module.scss @@ -36,4 +36,9 @@ .timeContainer { margin-left: auto; padding-left: 16px; + color: var(--black-400); + font-size: 12px; + font-weight: 500; + line-height: 16px; + align-self: flex-start; } diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx index 00cfa019..45eeffda 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx @@ -1,5 +1,4 @@ import { clsx } from 'clsx' -import styles from './NotificationView.module.scss' import type { Notification } from '../../../graphql/types.gen' import { createMemo, createSignal, onMount, Show } from 'solid-js' import { NotificationType } from '../../../graphql/types.gen' @@ -9,10 +8,13 @@ import { useNotifications } from '../../../context/notifications' import { Userpic } from '../../Author/Userpic' import { useLocalize } from '../../../context/localize' import type { ArticlePageSearchParams } from '../../Article/FullArticle' +import { TimeAgo } from '../../_shared/TimeAgo' +import styles from './NotificationView.module.scss' type Props = { notification: Notification onClick: () => void + dateTimeFormat: 'ago' | 'time' | 'date' class?: string } @@ -37,7 +39,7 @@ export const NotificationView = (props: Props) => { const { changeSearchParam } = useRouter() - const { t } = useLocalize() + const { t, formatDate, formatTime } = useLocalize() const [data, setData] = createSignal(null) @@ -136,6 +138,20 @@ export const NotificationView = (props: Props) => { } } + const formattedDateTime = createMemo(() => { + switch (props.dateTimeFormat) { + case 'ago': { + return + } + case 'time': { + return formatTime(new Date(props.notification.createdAt)) + } + case 'date': { + return formatDate(new Date(props.notification.createdAt), { month: 'numeric', year: '2-digit' }) + } + } + }) + return (
{ >
{content()}
-
- {/*{formatDate(new Date(props.notification.createdAt), { month: 'numeric' })}*/} -
+
{formattedDateTime()}
) diff --git a/src/components/NotificationsPanel/NotificationsPanel.module.scss b/src/components/NotificationsPanel/NotificationsPanel.module.scss index a1b18dd0..ee997569 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.module.scss +++ b/src/components/NotificationsPanel/NotificationsPanel.module.scss @@ -64,3 +64,12 @@ $transition-duration: 200ms; .emptyMessageContainer { text-align: center; } + +.periodTitle { + // TODO: check markup + margin: 32px 0 16px 0; + color: var(--black-400); + font-size: 12px; + font-weight: 500; + line-height: 14px; +} diff --git a/src/components/NotificationsPanel/NotificationsPanel.tsx b/src/components/NotificationsPanel/NotificationsPanel.tsx index c47e770f..3c7c09fa 100644 --- a/src/components/NotificationsPanel/NotificationsPanel.tsx +++ b/src/components/NotificationsPanel/NotificationsPanel.tsx @@ -4,7 +4,7 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler' import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' import { useLocalize } from '../../context/localize' import { Icon } from '../_shared/Icon' -import { createEffect, For } from 'solid-js' +import { createEffect, createMemo, For, Show } from 'solid-js' import { useNotifications } from '../../context/notifications' import { NotificationView } from './NotificationView' import { EmptyMessage } from './EmptyMessage' @@ -14,6 +14,30 @@ type Props = { onClose: () => void } +const getYesterdayStart = () => { + const now = new Date() + return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0, 0) +} + +const isSameDate = (date1: Date, date2: Date) => + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + +const isToday = (date: Date) => { + return isSameDate(date, new Date()) +} + +const isYesterday = (date: Date) => { + const yesterday = getYesterdayStart() + return isSameDate(date, yesterday) +} + +const isEarlier = (date: Date) => { + const yesterday = getYesterdayStart() + return date.getTime() < yesterday.getTime() +} + export const NotificationsPanel = (props: Props) => { const { t } = useLocalize() const { sortedNotifications } = useNotifications() @@ -55,6 +79,18 @@ export const NotificationsPanel = (props: Props) => { handleHide() } + const todayNotifications = createMemo(() => { + return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt))) + }) + + const yesterdayNotifications = createMemo(() => { + return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt))) + }) + + const earlierNotifications = createMemo(() => { + return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt))) + }) + return (
{
{t('Notifications')}
- }> - {(notification) => ( - - )} - + 0} fallback={}> + 0}> +
{t('today')}
+ + {(notification) => ( + + )} + +
+ 0}> +
{t('yesterday')}
+ + {(notification) => ( + + )} + +
+ 0}> +
{t('earlier')}
+ + {(notification) => ( + + )} + +
+
) diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index da355cf5..6f05aac4 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -1,5 +1,3 @@ -import { capitalize } from '../../utils' -import styles from './Card.module.scss' import { createMemo, createSignal, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' @@ -12,6 +10,9 @@ import { Icon } from '../_shared/Icon' import { useLocalize } from '../../context/localize' import { CardTopic } from '../Feed/CardTopic' import { CheckButton } from '../_shared/CheckButton' +import { capitalize } from '../../utils/capitalize' + +import styles from './Card.module.scss' interface TopicProps { topic: Topic @@ -109,14 +110,6 @@ export const TopicCard = (props: TopicProps) => { {props.topic.body} - 0}> -
- {props.topic.stat?.shouts} публикаций -
-
{ ) return ( -
+
0}>
diff --git a/src/components/Views/AllTopics.module.scss b/src/components/Views/AllTopics.module.scss new file mode 100644 index 00000000..fe7b931c --- /dev/null +++ b/src/components/Views/AllTopics.module.scss @@ -0,0 +1,115 @@ +.allTopicsPage { + .group { + @include font-size(1.6rem); + + margin: 3em 0 9.6rem; + + @include media-breakpoint-down(sm) { + margin-bottom: 6.4rem; + } + + h2 { + margin-bottom: 3.2rem; + text-transform: capitalize; + + @include media-breakpoint-down(sm) { + margin-bottom: 1.6rem; + } + } + + .topic { + margin-bottom: 2.4rem; + } + } + + .container { + width: auto; + + .search-input { + display: inline-block; + width: 100px !important; + } + } +} + +.stats { + @include font-size(1.7rem); + + color: #9fa1a7; + display: flex; + margin: 0 0 1em; + + @include media-breakpoint-down(md) { + flex-wrap: wrap; + } + + @include media-breakpoint-down(sm) { + margin-top: 0.5em; + } + + .statsItem { + @include font-size(1.5rem); + + margin-right: 1.6rem; + white-space: nowrap; + + &:last-child { + margin-right: 0; + } + + &.compact { + font-size: small; + } + + &.followers { + word-break: keep-all; + } + + &.button { + float: right; + } + } +} + +.loadMoreContainer { + margin-top: 48px; + text-align: center; + + .loadMoreButton { + padding: 0.6em 3em; + width: 100%; + + @include media-breakpoint-up(sm) { + width: auto; + } + } +} + +.alphabet { + @include font-size(1.5rem); + color: rgba(0 0 0 / 20%); + display: flex; + flex-wrap: wrap; + font-weight: 700; + margin: 1.5em -3% 0 0; + + li { + min-width: 1.5em; + margin-right: 3%; + color: rgb(0 0 0 / 30%); + } + + a { + border: none; + } +} + +.articlesCounter { + @include font-size(1.2rem); + margin-left: 0.5em; + vertical-align: super; +} + +.viewSwitcher { + margin-bottom: 2rem; +} diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index d8ec3491..f9c16bc2 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -6,13 +6,13 @@ import { useRouter } from '../../stores/router' import { TopicCard } from '../Topic/Card' import { clsx } from 'clsx' import { useSession } from '../../context/session' -import styles from '../../styles/AllTopics.module.scss' import { SearchField } from '../_shared/SearchField' import { scrollHandler } from '../../utils/scroll' -import { StatMetrics } from '../_shared/StatMetrics' import { useLocalize } from '../../context/localize' import { dummyFilter } from '../../utils/dummyFilter' +import styles from './AllTopics.module.scss' + type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' } @@ -168,7 +168,17 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { showPublications={true} showDescription={true} /> - +
+ + {t('shoutsWithCount', { count: topic.stat.shouts })} + + + {t('authorsWithCount', { count: topic.stat.authors })} + + + {t('followersWithCount', { count: topic.stat.followers })} + +
)} diff --git a/src/components/_shared/Stat.module.scss b/src/components/_shared/Stat.module.scss deleted file mode 100644 index e40f76b5..00000000 --- a/src/components/_shared/Stat.module.scss +++ /dev/null @@ -1,36 +0,0 @@ -.statMetrics { - @include font-size(1.7rem); - color: #9fa1a7; - display: flex; - margin: 0 0 1em; - - @include media-breakpoint-down(md) { - flex-wrap: wrap; - } - - @include media-breakpoint-down(sm) { - margin-top: 0.5em; - } -} - -.statMetricsItem { - @include font-size(1.5rem); - margin-right: 1.6rem; - white-space: nowrap; - - &:last-child { - margin-right: 0; - } - - &.compact { - font-size: small; - } - - &.followers { - word-break: keep-all; - } - - &.button { - float: right; - } -} diff --git a/src/components/_shared/StatMetrics.tsx b/src/components/_shared/StatMetrics.tsx deleted file mode 100644 index de2697a5..00000000 --- a/src/components/_shared/StatMetrics.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { For } from 'solid-js' -import type { Stat, TopicStat } from '../../graphql/types.gen' -import { plural } from '../../utils' -import styles from './Stat.module.scss' -import { useLocalize } from '../../context/localize' - -interface StatMetricsProps { - fields?: string[] - stat: Stat | TopicStat - compact?: boolean -} - -const pseudonames = { - // topics: 'topics' # amount of topics for community💥 - followed: 'follower', - followers: 'follower', - rating: 'like', - viewed: 'view', - views: 'view', - reacted: 'involving', - reactions: 'involving', - commented: 'discussion', - comments: 'discussion', - shouts: 'post', - authors: 'author' -} - -export const StatMetrics = (props: StatMetricsProps) => { - const { t, lang } = useLocalize() - - return ( -
- - {(entity: string) => ( - - {props.stat[entity] + - ' ' + - t(pseudonames[entity] || entity.slice(-1)) + - plural(props.stat[entity] || 0, lang() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])} - - )} - -
- ) -} diff --git a/src/components/_shared/TimeAgo/TimeAgo.module.scss b/src/components/_shared/TimeAgo/TimeAgo.module.scss new file mode 100644 index 00000000..6aee4238 --- /dev/null +++ b/src/components/_shared/TimeAgo/TimeAgo.module.scss @@ -0,0 +1,3 @@ +.TimeAgo { + white-space: nowrap; +} diff --git a/src/components/_shared/TimeAgo/TimeAgo.tsx b/src/components/_shared/TimeAgo/TimeAgo.tsx new file mode 100644 index 00000000..cb1cca0a --- /dev/null +++ b/src/components/_shared/TimeAgo/TimeAgo.tsx @@ -0,0 +1,37 @@ +import { clsx } from 'clsx' +import { useLocalize } from '../../../context/localize' +import { createSignal, onCleanup, onMount } from 'solid-js' + +import styles from './TimeAgo.module.scss' + +type Props = { + date: any + class?: string +} + +export const TimeAgo = (props: Props) => { + const { formatDate, formatTimeAgo } = useLocalize() + const [formattedTimeAgo, setFormattedTimeAgo] = createSignal(formatTimeAgo(new Date(props.date))) + + onMount(() => { + let timerId: NodeJS.Timeout + const updateTimeAgo = () => { + timerId = setTimeout(() => { + setFormattedTimeAgo(formatTimeAgo(new Date(props.date))) + updateTimeAgo() + }, 1000) + } + updateTimeAgo() + + onCleanup(() => clearTimeout(timerId)) + }) + + return ( +
+ {formattedTimeAgo()} +
+ ) +} diff --git a/src/components/_shared/TimeAgo/index.ts b/src/components/_shared/TimeAgo/index.ts new file mode 100644 index 00000000..0c64b085 --- /dev/null +++ b/src/components/_shared/TimeAgo/index.ts @@ -0,0 +1 @@ +export { TimeAgo } from './TimeAgo' diff --git a/src/context/localize.tsx b/src/context/localize.tsx index 166da80b..c307bb6e 100644 --- a/src/context/localize.tsx +++ b/src/context/localize.tsx @@ -1,14 +1,23 @@ import type { i18n } from 'i18next' import type { Accessor, JSX } from 'solid-js' -import { createContext, createEffect, createSignal, Show, useContext } from 'solid-js' +import { createContext, createEffect, createMemo, createSignal, Show, useContext } from 'solid-js' import { useRouter } from '../stores/router' import i18next, { changeLanguage, t } from 'i18next' import Cookie from 'js-cookie' +import TimeAgo from 'javascript-time-ago' +import en from 'javascript-time-ago/locale/en' +import ru from 'javascript-time-ago/locale/ru' + +TimeAgo.addLocale(en) +TimeAgo.addLocale(ru) type LocalizeContextType = { t: i18n['t'] lang: Accessor setLang: (lang: Language) => void + formatTime: (date: Date, options?: Intl.DateTimeFormatOptions) => string + formatDate: (date: Date, options?: Intl.DateTimeFormatOptions) => string + formatTimeAgo: (date: Date) => string } export type Language = 'ru' | 'en' @@ -21,7 +30,9 @@ export function useLocalize() { export const LocalizeProvider = (props: { children: JSX.Element }) => { const [lang, setLang] = createSignal(i18next.language === 'en' ? 'en' : 'ru') - const { searchParams, changeSearchParam } = useRouter<{ lng: string }>() + const { searchParams, changeSearchParam } = useRouter<{ + lng: string + }>() createEffect(() => { if (!searchParams().lng) { @@ -36,7 +47,43 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => { changeSearchParam({ lng: null }, true) }) - const value: LocalizeContextType = { t, lang, setLang } + const formatTime = (date: Date, options: Intl.DateTimeFormatOptions = {}) => { + const opts = Object.assign( + {}, + { + hour: '2-digit', + minute: '2-digit' + }, + options + ) + + return date.toLocaleTimeString(lang(), opts) + } + + const formatDate = (date: Date, options: Intl.DateTimeFormatOptions = {}) => { + const opts = Object.assign( + {}, + { + month: 'long', + day: 'numeric', + year: 'numeric' + }, + options + ) + + let result = date.toLocaleDateString(lang(), opts) + if (lang() === 'ru') { + result = result.replace(' г.', '') + } + + return result + } + + const timeAgo = createMemo(() => new TimeAgo(lang())) + + const formatTimeAgo = (date: Date) => timeAgo().format(date) + + const value: LocalizeContextType = { t, lang, setLang, formatTime, formatDate, formatTimeAgo } return ( diff --git a/src/renderer/_default.page.server.tsx b/src/renderer/_default.page.server.tsx index 1501e428..fa656cef 100644 --- a/src/renderer/_default.page.server.tsx +++ b/src/renderer/_default.page.server.tsx @@ -4,10 +4,11 @@ import { App } from '../components/App' import { initRouter } from '../stores/router' import type { PageContext } from './types' import { MetaProvider, renderTags } from '@solidjs/meta' -import i18next, { changeLanguage, init as initI18next } from 'i18next' +import i18next from 'i18next' import ru from '../../public/locales/ru/translation.json' import en from '../../public/locales/en/translation.json' import type { Language } from '../context/localize' +import ICU from 'i18next-icu' export const passToClient = ['pageProps', 'lng', 'documentProps', 'is404'] @@ -32,7 +33,7 @@ export const render = async (pageContext: PageContext) => { if (!i18next.isInitialized) { // eslint-disable-next-line import/no-named-as-default-member - await initI18next({ + await i18next.use(ICU).init({ // debug: true, supportedLngs: ['ru', 'en'], fallbackLng: lng, @@ -44,7 +45,7 @@ export const render = async (pageContext: PageContext) => { } }) } else if (i18next.language !== lng) { - await changeLanguage(lng) + await i18next.changeLanguage(lng) } if (pageContext.is404) { diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 431c5a84..f639cf24 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -355,7 +355,7 @@ export const apiClient = { }, getNotifications: async (params: NotificationsQueryParams): Promise => { const resp = await privateGraphQLClient.query(notifications, params).toPromise() - console.debug(resp.data) + // console.debug(resp.data) return resp.data.loadNotifications }, markNotificationAsRead: async (notificationId: number): Promise => { diff --git a/src/utils/capitalize.ts b/src/utils/capitalize.ts new file mode 100644 index 00000000..8e4ac45d --- /dev/null +++ b/src/utils/capitalize.ts @@ -0,0 +1,9 @@ +export const capitalize = (originalString: string, firstonly = false) => { + const s = originalString.trim() + return firstonly + ? s.charAt(0).toUpperCase() + s.slice(1) + : s + .split(' ') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' ') +} diff --git a/src/utils/dummyFilter.ts b/src/utils/dummyFilter.ts index fe4d8f4b..3b68b38b 100644 --- a/src/utils/dummyFilter.ts +++ b/src/utils/dummyFilter.ts @@ -1,7 +1,6 @@ import { translit } from './ru2en' import { Author, Topic } from '../graphql/types.gen' - -type SearchData = Array +import { isAuthor } from './isAuthor' const prepareQuery = (searchQuery, lang) => { const q = searchQuery.toLowerCase() @@ -14,9 +13,16 @@ const stringMatches = (str, q, lang) => { return preparedStr.split(' ').some((word) => word.startsWith(q)) } -export const dummyFilter = (data: SearchData, searchQuery: string, lang: 'ru' | 'en'): SearchData => { +export const dummyFilter = ( + data: T[], + searchQuery: string, + lang: 'ru' | 'en' +): T[] => { const q = prepareQuery(searchQuery, lang) - if (q.length === 0) return data + + if (q.length === 0) { + return data + } return data.filter((item) => { const slugMatches = item.slug && item.slug.split('-').some((w) => w.startsWith(q)) @@ -26,9 +32,10 @@ export const dummyFilter = (data: SearchData, searchQuery: string, lang: 'ru' | return stringMatches(item.title, q, lang) } - if ('name' in item) { + if (isAuthor(item)) { return stringMatches(item.name, q, lang) || (item.bio && stringMatches(item.bio, q, lang)) } + // If it does not match any of the 'slug', 'title', 'name' , 'bio' fields // current element should not be included in the filtered array return false diff --git a/src/utils/formatDateTime.ts b/src/utils/formatDateTime.ts deleted file mode 100644 index 24f00a37..00000000 --- a/src/utils/formatDateTime.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Accessor, createMemo } from 'solid-js' -import { useLocalize } from '../context/localize' - -// unix timestamp in seconds -const formattedTime = (time: number): Accessor => { - // FIXME: maybe it's better to move it from here - const { lang } = useLocalize() - - return createMemo(() => { - return new Date(time).toLocaleTimeString(lang(), { - hour: 'numeric', - minute: 'numeric' - }) - }) -} - -export default formattedTime diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index 2da4dc2c..00000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -export const reflow = () => document.body.clientWidth - -export const unique = (v) => { - const s = new Set(v) - return [...s] -} - -export const preventSmoothScrollOnTabbing = () => { - document.addEventListener('keydown', (e) => { - if (e.key !== 'Tab') return - - document.documentElement.style.scrollBehavior = '' - - setTimeout(() => { - document.documentElement.style.scrollBehavior = 'smooth' - }) - }) -} - -export const capitalize = (originalString: string, firstonly = false) => { - const s = originalString.trim() - return firstonly - ? s.charAt(0).toUpperCase() + s.slice(1) - : s - .split(' ') - .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) - .join(' ') -} - -export const plural = (amount: number, w: string[]) => { - try { - const a = amount.toString() - const x = Number.parseInt(a.at(-1)) - const xx = Number.parseInt(a.at(-2) + a.at(-1)) - - if (xx > 5 && xx < 20) return w[0] - - if (x === 1) return w[1] - - if (x > 1 && x < 5) return w[2] - } catch (error) { - console.error('[utils] plural error', error) - } - - return w[0] -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const shuffle = (items: any[]) => { - const cached = [...items] - let temp - let i = cached.length - let rand - - while (--i) { - rand = Math.floor(i * Math.random()) - temp = cached[rand] - cached[rand] = cached[i] - cached[i] = temp - } - - return cached -} - -export const snake2camel = (s: string) => - s - .split(/(?=[A-Z])/) - .join('-') - .toLowerCase() - -export const formatDate = (date: Date, options: Intl.DateTimeFormatOptions = {}) => { - const opts = Object.assign( - {}, - { - month: 'long', - day: 'numeric', - year: 'numeric' - }, - options - ) - - return date.toLocaleDateString('ru', opts).replace(' г.', '') -}