From 3a7d138eef0b4be7ea98c7d2e3495882a48f5763 Mon Sep 17 00:00:00 2001 From: Igor Lobanov Date: Wed, 20 Dec 2023 09:07:57 +0100 Subject: [PATCH 1/7] feed period select (#340) * feed period select * fix, unused code removed * Fix styles * Fix styles * Fix styles --------- Co-authored-by: Igor Lobanov Co-authored-by: ilya-bkv --- public/locales/en/translation.json | 3 + public/locales/ru/translation.json | 3 + src/components/Article/FullArticle.tsx | 6 +- src/components/AuthGuard/AuthGuard.tsx | 4 +- .../Author/AuthorBadge/AuthorBadge.tsx | 4 +- .../Author/AuthorCard/AuthorCard.tsx | 4 +- src/components/Discours/Hero.tsx | 4 +- .../Feed/ArticleCard/ArticleCard.tsx | 4 +- .../Nav/AuthModal/ForgotPasswordForm.tsx | 6 +- src/components/Nav/AuthModal/LoginForm.tsx | 6 +- src/components/Nav/AuthModal/RegisterForm.tsx | 6 +- .../NotificationView/NotificationView.tsx | 4 +- src/components/Views/AllAuthors.tsx | 4 +- src/components/Views/AllTopics.tsx | 4 +- src/components/Views/Expo/Expo.tsx | 2 +- src/components/Views/Feed/Feed.module.scss | 25 +++- src/components/Views/Feed/Feed.tsx | 125 ++++++++++++++---- src/components/Views/Home.tsx | 12 -- src/components/Views/Inbox.tsx | 6 +- src/components/Views/Topic.tsx | 10 +- .../_shared/DropDown/DropDown.module.scss | 7 + src/components/_shared/DropDown/DropDown.tsx | 69 ++++++++++ src/components/_shared/DropDown/index.ts | 1 + src/components/_shared/Popup/Popup.tsx | 4 +- src/context/localize.tsx | 4 +- src/pages/feed.page.tsx | 5 +- src/stores/router.ts | 4 +- src/stores/ui.ts | 6 +- src/styles/app.scss | 10 +- 29 files changed, 260 insertions(+), 92 deletions(-) create mode 100644 src/components/_shared/DropDown/DropDown.module.scss create mode 100644 src/components/_shared/DropDown/DropDown.tsx create mode 100644 src/components/_shared/DropDown/index.ts diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6b7a4173..35c0913e 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -357,9 +357,12 @@ "This comment has not yet been rated": "This comment has not yet been rated", "This email is already taken. If it's you": "This email is already taken. If it's you", "This functionality is currently not available, we would like to work on this issue. Use the download link.": "This functionality is currently not available, we would like to work on this issue. Use the download link.", + "This month": "This month", "This post has not been rated yet": "This post has not been rated yet", "This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted": "This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted", "This way you ll be able to subscribe to authors, interesting topics and customize your feed": "This way you ll be able to subscribe to authors, interesting topics and customize your feed", + "This week": "This week", + "This year": "This year", "To leave a comment please": "To leave a comment please", "To write a comment, you must": "To write a comment, you must", "Top authors": "Authors rating", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 5f9fd460..b496a982 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -377,9 +377,12 @@ "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", "This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы", "This functionality is currently not available, we would like to work on this issue. Use the download link.": "В данный момент этот функционал не доступен, бы работаем над этой проблемой. Воспользуйтесь загрузкой по ссылке.", + "This month": "За месяц", "This post has not been rated yet": "Эту публикацию еще пока никто не оценил", "This way we ll realize that you re a real person and ll take your vote into account. And you ll see how others voted": "Так мы поймем, что вы реальный человек, и учтем ваш голос. А вы увидите, как проголосовали другие", "This way you ll be able to subscribe to authors, interesting topics and customize your feed": "Так вы сможете подписаться на авторов, интересные темы и настроить свою ленту", + "This week": "За неделю", + "This year": "За год", "To leave a comment please": "Чтобы оставить комментарий, необходимо", "To write a comment, you must": "Чтобы написать комментарий, необходимо", "Top authors": "Рейтинг авторов", diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 51ea46f4..2fc420ce 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -135,7 +135,7 @@ export const FullArticle = (props: Props) => { scrollTo(commentsRef.current) } - const { searchParams, changeSearchParam } = useRouter() + const { searchParams, changeSearchParams } = useRouter() createEffect(() => { if (props.scrollToComments) { @@ -146,7 +146,7 @@ export const FullArticle = (props: Props) => { createEffect(() => { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { scrollToComments() - changeSearchParam({ + changeSearchParams({ scrollTo: null, }) } @@ -158,7 +158,7 @@ export const FullArticle = (props: Props) => { `[id='comment_${searchParams().commentId}']`, ) - changeSearchParam({ commentId: null }) + changeSearchParams({ commentId: null }) if (commentElement) { scrollTo(commentElement) diff --git a/src/components/AuthGuard/AuthGuard.tsx b/src/components/AuthGuard/AuthGuard.tsx index c7b0c442..d4e2e133 100644 --- a/src/components/AuthGuard/AuthGuard.tsx +++ b/src/components/AuthGuard/AuthGuard.tsx @@ -13,7 +13,7 @@ type Props = { export const AuthGuard = (props: Props) => { const { isAuthenticated, isSessionLoaded } = useSession() - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() createEffect(() => { if (props.disabled) { @@ -23,7 +23,7 @@ export const AuthGuard = (props: Props) => { if (isAuthenticated()) { hideModal() } else { - changeSearchParam( + changeSearchParams( { source: 'authguard', modal: 'auth', diff --git a/src/components/Author/AuthorBadge/AuthorBadge.tsx b/src/components/Author/AuthorBadge/AuthorBadge.tsx index 9ab3f66b..b744df32 100644 --- a/src/components/Author/AuthorBadge/AuthorBadge.tsx +++ b/src/components/Author/AuthorBadge/AuthorBadge.tsx @@ -29,7 +29,7 @@ export const AuthorBadge = (props: Props) => { subscriptions, actions: { loadSubscriptions, requireAuthentication }, } = useSession() - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const { t, formatDate } = useLocalize() const subscribed = createMemo(() => subscriptions().authors.some((author) => author.slug === props.author.slug), @@ -54,7 +54,7 @@ export const AuthorBadge = (props: Props) => { const initChat = () => { requireAuthentication(() => { openPage(router, `inbox`) - changeSearchParam({ + changeSearchParams({ initChat: props.author.id.toString(), }) }, 'discussions') diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 65f4353a..a1269d82 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -72,11 +72,11 @@ export const AuthorCard = (props: Props) => { }) // TODO: reimplement AuthorCard - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const initChat = () => { requireAuthentication(() => { openPage(router, `inbox`) - changeSearchParam({ + changeSearchParams({ initChat: props.author.id.toString(), }) }, 'discussions') diff --git a/src/components/Discours/Hero.tsx b/src/components/Discours/Hero.tsx index 27be6cc3..105300cc 100644 --- a/src/components/Discours/Hero.tsx +++ b/src/components/Discours/Hero.tsx @@ -7,7 +7,7 @@ import styles from './Hero.module.scss' export default () => { const { t } = useLocalize() - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() return (
@@ -28,7 +28,7 @@ export default () => { class="button" onClick={() => { showModal('auth') - changeSearchParam({ + changeSearchParams({ mode: 'register', }) }} diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 00654f34..f656c8a7 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -98,11 +98,11 @@ export const ArticleCard = (props: ArticleCardProps) => { const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const scrollToComments = (event) => { event.preventDefault() openPage(router, 'article', { slug: props.article.slug }) - changeSearchParam({ + changeSearchParams({ scrollTo: 'comments', }) } diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 31c59da7..eb27ab16 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -20,7 +20,7 @@ type FormFields = { type ValidationErrors = Partial> export const ForgotPasswordForm = () => { - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const { t, lang } = useLocalize() const handleEmailInput = (newEmail: string) => { setValidationErrors(({ email: _notNeeded, ...rest }) => rest) @@ -119,7 +119,7 @@ export const ForgotPasswordForm = () => { href="#" onClick={(event) => { event.preventDefault() - changeSearchParam({ + changeSearchParams({ mode: 'register', }) }} @@ -141,7 +141,7 @@ export const ForgotPasswordForm = () => { - changeSearchParam({ + changeSearchParams({ mode: 'login', }) } diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 25f45594..0a7e11fc 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -47,7 +47,7 @@ export const LoginForm = () => { actions: { signIn }, } = useSession() - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const [password, setPassword] = createSignal('') @@ -201,7 +201,7 @@ export const LoginForm = () => { - changeSearchParam({ + changeSearchParams({ mode: 'forgot-password', }) } @@ -218,7 +218,7 @@ export const LoginForm = () => { - changeSearchParam({ + changeSearchParams({ mode: 'register', }) } diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index d2f9c9f2..7f642d3e 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -32,7 +32,7 @@ const handleEmailInput = (newEmail: string) => { } export const RegisterForm = () => { - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const { t } = useLocalize() const { emailChecks } = useEmailChecks() @@ -202,7 +202,7 @@ export const RegisterForm = () => { href="#" onClick={(event) => { event.preventDefault() - changeSearchParam({ + changeSearchParams({ mode: 'login', }) }} @@ -255,7 +255,7 @@ export const RegisterForm = () => { - changeSearchParam({ + changeSearchParams({ mode: 'login', }) } diff --git a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx index 5751884b..c611f4f3 100644 --- a/src/components/NotificationsPanel/NotificationView/NotificationView.tsx +++ b/src/components/NotificationsPanel/NotificationView/NotificationView.tsx @@ -42,7 +42,7 @@ export const NotificationView = (props: Props) => { actions: { markNotificationAsRead, hideNotificationsPanel }, } = useNotifications() - const { changeSearchParam } = useRouter() + const { changeSearchParams } = useRouter() const { t, formatDate, formatTime } = useLocalize() @@ -139,7 +139,7 @@ export const NotificationView = (props: Props) => { openPage(router, 'article', { slug: data().shout.slug }) if (data().reactionIds) { - changeSearchParam({ commentId: data().reactionIds[0].toString() }) + changeSearchParams({ commentId: data().reactionIds[0].toString() }) } } diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index b3d40f15..f3d059a3 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -31,7 +31,7 @@ const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫ export const AllAuthorsView = (props: Props) => { const { t, lang } = useLocalize() const [limit, setLimit] = createSignal(PAGE_SIZE) - const { searchParams, changeSearchParam } = useRouter() + const { searchParams, changeSearchParams } = useRouter() const { sortedAuthors } = useAuthorsStore({ authors: props.authors, sortBy: searchParams().by || 'shouts', @@ -41,7 +41,7 @@ export const AllAuthorsView = (props: Props) => { createEffect(() => { if (!searchParams().by) { - changeSearchParam({ + changeSearchParams({ by: 'shouts', }) } diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 6fad3908..6c7e6b30 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -31,7 +31,7 @@ const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫ export const AllTopicsView = (props: Props) => { const { t, lang } = useLocalize() - const { searchParams, changeSearchParam } = useRouter() + const { searchParams, changeSearchParams } = useRouter() const [limit, setLimit] = createSignal(PAGE_SIZE) const { sortedTopics } = useTopicsStore({ @@ -43,7 +43,7 @@ export const AllTopicsView = (props: Props) => { createEffect(() => { if (!searchParams().by) { - changeSearchParam({ + changeSearchParams({ by: 'shouts', }) } diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 76a06530..52d61cd9 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -46,7 +46,7 @@ export const Expo = (props: Props) => { }) const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => { - const filters = { ...additionalFilters } + const filters = { visibility: 'public', ...additionalFilters } if (props.layout) { filters.layout = props.layout diff --git a/src/components/Views/Feed/Feed.module.scss b/src/components/Views/Feed/Feed.module.scss index 87fc5f6e..5c97c2ca 100644 --- a/src/components/Views/Feed/Feed.module.scss +++ b/src/components/Views/Feed/Feed.module.scss @@ -1,7 +1,4 @@ .feedFilter { - margin-bottom: 4.8rem; - margin-top: 0.2em; - @include media-breakpoint-down(md) { margin-right: 4rem !important; } @@ -192,3 +189,25 @@ font-weight: 500; } + +.filtersContainer { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4rem; + + .feedFilter { + margin-top: 0; + margin-bottom: 0; + + & > li { + margin-bottom: 0; + } + } +} + +.periodSwitcher { + font-size: 14px; + font-weight: 700; + line-height: 18px; +} diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 3ac8822e..6629bdf6 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -3,7 +3,7 @@ import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphq import { getPagePath } from '@nanostores/router' import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' -import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, on, onMount, Show } from 'solid-js' import { useLocalize } from '../../../context/localize' import { useReactions } from '../../../context/reactions' @@ -13,6 +13,8 @@ import { useTopAuthorsStore } from '../../../stores/zine/topAuthors' import { useTopicsStore } from '../../../stores/zine/topics' import { apiClient } from '../../../utils/apiClient' import { getImageUrl } from '../../../utils/getImageUrl' +import { getServerDate } from '../../../utils/getServerDate' +import { DropDown } from '../../_shared/DropDown' import { Icon } from '../../_shared/Icon' import { Loading } from '../../_shared/Loading' import { CommentDate } from '../../Article/CommentDate' @@ -28,8 +30,16 @@ import stylesTopic from '../../Feed/CardTopic.module.scss' export const FEED_PAGE_SIZE = 20 const UNRATED_ARTICLES_COUNT = 5 +type FeedPeriod = 'week' | 'month' | 'year' + +type PeriodItem = { + value: FeedPeriod + title: string +} + type FeedSearchParams = { by: 'publish_date' | 'rating' | 'last_comment' + period: FeedPeriod } const getOrderBy = (by: FeedSearchParams['by']) => { @@ -53,7 +63,16 @@ type Props = { export const Feed = (props: Props) => { const { t } = useLocalize() - const { page, searchParams } = useRouter() + + const monthPeriod: PeriodItem = { value: 'month', title: t('This month') } + + const periods: PeriodItem[] = [ + { value: 'week', title: t('This week') }, + monthPeriod, + { value: 'year', title: t('This year') }, + ] + + const { page, searchParams, changeSearchParams } = useRouter() const [isLoading, setIsLoading] = createSignal(false) const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false) @@ -64,6 +83,16 @@ export const Feed = (props: Props) => { const [topComments, setTopComments] = createSignal([]) const [unratedArticles, setUnratedArticles] = createSignal([]) + const currentPeriod = createMemo(() => { + const period = periods.find((p) => p.value === searchParams().period) + + if (!period) { + return monthPeriod + } + + return period + }) + const { actions: { loadReactionsBy }, } = useReactions() @@ -86,7 +115,7 @@ export const Feed = (props: Props) => { createEffect( on( - () => page().route + searchParams().by, + () => page().route + searchParams().by + searchParams().period, () => { resetSortedArticles() loadMore() @@ -94,6 +123,21 @@ export const Feed = (props: Props) => { { defer: true }, ), ) + + const getFromDate = (period: FeedPeriod): Date => { + const now = new Date() + switch (period) { + case 'week': { + return new Date(now.setDate(now.getDate() - 7)) + } + case 'month': { + return new Date(now.setMonth(now.getMonth() - 1)) + } + case 'year': { + return new Date(now.setFullYear(now.getFullYear() - 1)) + } + } + } const loadFeedShouts = () => { const options: LoadShoutsOptions = { limit: FEED_PAGE_SIZE, @@ -106,6 +150,12 @@ export const Feed = (props: Props) => { options.order_by = orderBy } + if (searchParams().by && searchParams().by !== 'publish_date') { + const period = searchParams().period || 'month' + const fromDate = getFromDate(period) + options.filters = { fromDate: getServerDate(fromDate) } + } + return props.loadShouts(options) } @@ -148,32 +198,49 @@ export const Feed = (props: Props) => {
- +
+
    +
  • + {t('Recent')} +
  • + {/*
  • */} + {/* {t('Most read')}*/} + {/*
  • */} +
  • + changeSearchParams({ by: 'rating' })}> + {t('Top rated')} + +
  • +
  • + changeSearchParams({ by: 'last_comment' })}> + {t('Most commented')} + +
  • +
+ +
+ changeSearchParams({ period: period.value })} + /> +
+
+
}> 0}> diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 952bf1da..a7b69137 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -115,9 +115,7 @@ export const HomeView = (props: Props) => { wrapper={'top-article'} nodate={true} /> - - { wrapper={'author'} nodate={true} /> - - - - { header={

{t('Top commented')}

} nodate={true} /> - {randomLayout()} - - { isTopicCompact={true} nodate={true} /> - - - diff --git a/src/components/Views/Inbox.tsx b/src/components/Views/Inbox.tsx index 755c39a4..348b7207 100644 --- a/src/components/Views/Inbox.tsx +++ b/src/components/Views/Inbox.tsx @@ -52,7 +52,7 @@ export const InboxView = () => { const [isClear, setClear] = createSignal(false) const { session } = useSession() const currentUserId = createMemo(() => session()?.user.id) - const { changeSearchParam, searchParams } = useRouter() + const { changeSearchParams, searchParams } = useRouter() // Поиск по диалогам const getQuery = (query) => { if (query().length >= 2) { @@ -67,7 +67,7 @@ export const InboxView = () => { const handleOpenChat = async (chat: Chat) => { setCurrentDialog(chat) - changeSearchParam({ + changeSearchParams({ chat: chat.id, }) try { @@ -126,7 +126,7 @@ export const InboxView = () => { try { const newChat = await createChat([Number(searchParams().initChat)], '') await loadChats() - changeSearchParam({ + changeSearchParams({ initChat: null, chat: newChat.chat.id, }) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 27717294..b830a1a6 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -41,7 +41,7 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: Props) => { const { t } = useLocalize() - const { searchParams, changeSearchParam } = useRouter() + const { searchParams, changeSearchParams } = useRouter() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) @@ -125,7 +125,7 @@ export const TopicView = (props: Props) => { */} {/**/} {/*
  • */} - {/* */} {/*
  • */} {/*
  • */} - {/* */} {/*
  • */} diff --git a/src/components/_shared/DropDown/DropDown.module.scss b/src/components/_shared/DropDown/DropDown.module.scss new file mode 100644 index 00000000..c438b02b --- /dev/null +++ b/src/components/_shared/DropDown/DropDown.module.scss @@ -0,0 +1,7 @@ +.chevron { + vertical-align: top; + + &.rotate { + transform: rotate(180deg); + } +} diff --git a/src/components/_shared/DropDown/DropDown.tsx b/src/components/_shared/DropDown/DropDown.tsx new file mode 100644 index 00000000..e1ba7aa4 --- /dev/null +++ b/src/components/_shared/DropDown/DropDown.tsx @@ -0,0 +1,69 @@ +import type { PopupProps } from '../Popup' + +import { clsx } from 'clsx' +import { createSignal, For, Show } from 'solid-js' + +import { Popup } from '../Popup' + +import styles from './DropDown.module.scss' + +export type Option = { + value: string | number + title: string +} + +type Props = { + class?: string + popupProps?: PopupProps + options: TOption[] + currentOption: TOption + triggerCssClass?: string + onChange: (option: TOption) => void +} + +const Chevron = (props: { class?: string }) => { + return ( + + + + ) +} + +export const DropDown = (props: Props) => { + const [isPopupVisible, setIsPopupVisible] = createSignal(false) + + return ( + + + {props.currentOption.title}{' '} + +
    + } + variant="tiny" + onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)} + {...props.popupProps} + > + p.value !== props.currentOption.value)}> + {(option) => ( + + )} + + + + ) +} diff --git a/src/components/_shared/DropDown/index.ts b/src/components/_shared/DropDown/index.ts new file mode 100644 index 00000000..8a266f72 --- /dev/null +++ b/src/components/_shared/DropDown/index.ts @@ -0,0 +1 @@ +export { DropDown } from './DropDown' diff --git a/src/components/_shared/Popup/Popup.tsx b/src/components/_shared/Popup/Popup.tsx index 78d5cdde..f9cb0902 100644 --- a/src/components/_shared/Popup/Popup.tsx +++ b/src/components/_shared/Popup/Popup.tsx @@ -32,7 +32,9 @@ export const Popup = (props: PopupProps) => { useOutsideClickHandler({ containerRef, predicate: () => isVisible(), - handler: () => setIsVisible(false), + handler: () => { + setIsVisible(false) + }, }) const toggle = () => setIsVisible((oldVisible) => !oldVisible) diff --git a/src/context/localize.tsx b/src/context/localize.tsx index 394526e8..8dcc6cd5 100644 --- a/src/context/localize.tsx +++ b/src/context/localize.tsx @@ -32,7 +32,7 @@ export function useLocalize() { export const LocalizeProvider = (props: { children: JSX.Element }) => { const [lang, setLang] = createSignal(i18next.language === 'en' ? 'en' : 'ru') - const { searchParams, changeSearchParam } = useRouter<{ + const { searchParams, changeSearchParams } = useRouter<{ lng: string }>() @@ -46,7 +46,7 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => { changeLanguage(lng) setLang(lng) Cookie.set('lng', lng) - changeSearchParam({ lng: null }, true) + changeSearchParams({ lng: null }, true) }) const formatTime = (date: Date, options: Intl.DateTimeFormatOptions = {}) => { diff --git a/src/pages/feed.page.tsx b/src/pages/feed.page.tsx index 3daf019d..9b1a099f 100644 --- a/src/pages/feed.page.tsx +++ b/src/pages/feed.page.tsx @@ -12,7 +12,10 @@ import { loadMyFeed, loadShouts, resetSortedArticles } from '../stores/zine/arti const handleFeedLoadShouts = (options: LoadShoutsOptions) => { return loadShouts({ ...options, - filters: { visibility: 'community' }, + filters: { + visibility: 'community', + ...options.filters, + }, }) } diff --git a/src/stores/router.ts b/src/stores/router.ts index 4803ed82..a9409c68 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -138,7 +138,7 @@ export const useRouter = = Record< const page = useStore(routerStore) const searchParams = useStore(searchParamsStore) as unknown as Accessor - const changeSearchParam = (newValues: Partial, replace = false) => { + const changeSearchParams = (newValues: Partial, replace = false) => { const newSearchParams = { ...searchParamsStore.get() } Object.keys(newValues).forEach((key) => { @@ -155,6 +155,6 @@ export const useRouter = = Record< return { page, searchParams, - changeSearchParam, + changeSearchParams, } } diff --git a/src/stores/ui.ts b/src/stores/ui.ts index aa555272..b5630fc0 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -42,13 +42,13 @@ export const MODALS: Record = { const [modal, setModal] = createSignal(null) -const { searchParams, changeSearchParam } = useRouter< +const { searchParams, changeSearchParams } = useRouter< AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams >() export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) => { if (modalSource) { - changeSearchParam({ + changeSearchParams({ source: modalSource, }) } @@ -70,7 +70,7 @@ export const hideModal = () => { newSearchParams.mode = null } - changeSearchParam(newSearchParams, true) + changeSearchParams(newSearchParams, true) setModal(null) } diff --git a/src/styles/app.scss b/src/styles/app.scss index 376ab52d..ef3ea022 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -204,7 +204,7 @@ a:hover, a:visited, a:link, .link { - border-bottom: 1px solid rgb(0 0 0 / 30%); + border-bottom: 2px solid rgb(0 0 0 / 30%); text-decoration: none; cursor: pointer; } @@ -624,6 +624,10 @@ figure { margin-bottom: 0.6em; white-space: nowrap; + .link { + border-bottom: none; + } + &:last-child { margin-right: 0; } @@ -645,9 +649,10 @@ figure { } a, + .link, .linkReplacement, button { - border-bottom: 2px solid transparent; + border-bottom: 1px solid transparent; color: var(--link-color); cursor: pointer; font-weight: inherit; @@ -662,6 +667,7 @@ figure { font-weight: bold; a, + .link, .linkReplacement, button { border-bottom: 2px solid #000; From 4a2f95aa550ec88cef1632a70fb1847dcabedf2e Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Wed, 20 Dec 2023 19:06:42 +0300 Subject: [PATCH 2/7] Feature/invite co authors modal (#341) * Change parent branch * Invite co-authors * resolve conversation * resolve conversation --- public/locales/en/translation.json | 12 ++++ public/locales/ru/translation.json | 12 ++++ src/components/Editor/Panel/Panel.tsx | 5 +- src/components/Views/Edit.tsx | 2 + .../Views/PublishSettings/PublishSettings.tsx | 23 ++---- .../DropdownSelect/DropdownSelect.module.scss | 71 +++++++++++++++++++ .../_shared/DropdownSelect/DropdownSelect.tsx | 61 ++++++++++++++++ .../_shared/DropdownSelect/index.ts | 1 + .../InviteCoAuthorsModal.tsx | 14 ++++ .../_shared/InviteCoAuthorsModal/index.ts | 1 + .../_shared/UserSearch/UserSearch.module.scss | 50 +++++++++++++ .../_shared/UserSearch/UserSearch.tsx | 61 ++++++++++++++++ src/components/_shared/UserSearch/index.ts | 1 + src/stores/ui.ts | 2 + 14 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 src/components/_shared/DropdownSelect/DropdownSelect.module.scss create mode 100644 src/components/_shared/DropdownSelect/DropdownSelect.tsx create mode 100644 src/components/_shared/DropdownSelect/index.ts create mode 100644 src/components/_shared/InviteCoAuthorsModal/InviteCoAuthorsModal.tsx create mode 100644 src/components/_shared/InviteCoAuthorsModal/index.ts create mode 100644 src/components/_shared/UserSearch/UserSearch.module.scss create mode 100644 src/components/_shared/UserSearch/UserSearch.tsx create mode 100644 src/components/_shared/UserSearch/index.ts diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 35c0913e..447108f5 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -2,6 +2,7 @@ "A guide to horizontal editorial: how an open journal works": "A guide to horizontal editorial: how an open journal works", "About": "About", "About the project": "About the project", + "Add": "Add", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title", "Add a link or click plus to embed media": "Add a link or click plus to embed media", "Add an embed widget": "Add an embed widget", @@ -59,6 +60,9 @@ "By title": "By title", "By updates": "By updates", "By views": "By views", + "Can make any changes, accept or reject suggestions, and share access with others": "Can make any changes, accept or reject suggestions, and share access with others", + "Can offer edits and comments, but cannot edit the post or share access with others": "Can offer edits and comments, but cannot edit the post or share access with others", + "Can write and edit text directly, and accept or reject suggestions from others": "Can write and edit text directly, and accept or reject suggestions from others", "Cancel": "Cancel", "Cancel changes": "Cancel changes", "Characters": "Знаков", @@ -66,11 +70,15 @@ "Choose a post type": "Choose a post type", "Choose a title image for the article. You can immediately see how the publication card will look like.": "Choose a title image for the article. You can immediately see how the publication card will look like.", "Choose who you want to write to": "Choose who you want to write to", + "Co-author": "Co-author", "Collaborate": "Help Edit", + "Collaborators": "Collaborators", "Collections": "Collections", "Come up with a subtitle for your story": "Come up with a subtitle for your story", "Come up with a title for your story": "Come up with a title for your story", + "Coming soon": "Coming soon", "Comment successfully deleted": "Comment successfully deleted", + "Commentator": "Commentator", "Comments": "Comments", "Communities": "Communities", "Community Discussion Rules": "Community Discussion Rules", @@ -119,6 +127,7 @@ "Edit": "Edit", "Edit profile": "Edit profile", "Editing": "Editing", + "Editor": "Editor", "Email": "Mail", "Enter": "Enter", "Enter URL address": "Enter URL address", @@ -191,6 +200,7 @@ "Invalid image URL": "Invalid image URL", "Invalid url format": "Invalid url format", "Invite co-authors": "Invite co-authors", + "Invite collaborators": "Invite collaborators", "Invite to collab": "Invite to Collab", "It does not look like url": "It doesn't look like a link", "Italic": "Italic", @@ -391,6 +401,7 @@ "Video": "Video", "Video format not supported": "Video format not supported", "Views": "Views", + "We are working on collaborative editing of articles and in the near future you will have an amazing opportunity - to create together with your colleagues": "We are working on collaborative editing of articles and in the near future you will have an amazing opportunity - to create together with your colleagues", "We can't find you, check email or": "We can't find you, check email or", "We know you, please try to login": "This email address is already registered, please try to login", "We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.", @@ -413,6 +424,7 @@ "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", + "Write your colleagues name or email": "Write your colleague's name or email", "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", "You were successfully authorized": "You were successfully authorized", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "You ll be able to participate in discussions, rate others' comments and learn about new responses", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index b496a982..5910a6e4 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -3,6 +3,7 @@ "A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя", "About": "О себе", "About the project": "О проекте", + "Add": "Добавить", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной", "Add a link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа", "Add an embed widget": "Добавить embed-виджет", @@ -62,6 +63,9 @@ "By title": "По названию", "By updates": "По обновлениям", "By views": "По просмотрам", + "Can make any changes, accept or reject suggestions, and share access with others": "Может вносить любые изменения, принимать и отклонять предложения, а также делиться доступом с другими", + "Can offer edits and comments, but cannot edit the post or share access with others": "Может предлагать правки и комментарии, но не может изменять пост и делиться доступом с другими", + "Can write and edit text directly, and accept or reject suggestions from others": "Может писать и редактировать текст напрямую, а также принимать или отклонять предложения других", "Cancel": "Отмена", "Cancel changes": "Отменить изменения", "Characters": "Знаков", @@ -69,12 +73,16 @@ "Choose a post type": "Выберите тип публикации", "Choose a title image for the article. You can immediately see how the publication card will look like.": "Выберите заглавное изображение для статьи. Тут же сразу можно увидеть как будет выглядеть карточка публикации.", "Choose who you want to write to": "Выберите кому хотите написать", + "Co-author": "Соавтор", "Collaborate": "Помочь редактировать", + "Collaborators": "Соавторы", "Collections": "Коллекции", "Come up with a subtitle for your story": "Придумайте подзаголовок вашей истории", "Come up with a title for your story": "Придумайте заголовок вашей истории", + "Coming soon": "Уже скоро", "Comment": "Комментировать", "Comment successfully deleted": "Комментарий успешно удален", + "Commentator": "Комментатор", "Comments": "Комментарии", "Communities": "Сообщества", "Community Discussion Rules": "Правила дискуссий в сообществе", @@ -125,6 +133,7 @@ "Edit profile": "Редактировать профиль", "Edited": "Отредактирован", "Editing": "Редактирование", + "Editor": "Редактор", "Email": "Почта", "Enter": "Войти", "Enter URL address": "Введите адрес ссылки", @@ -200,6 +209,7 @@ "Invalid image URL": "Некорректная ссылка на изображение", "Invalid url format": "Неверный формат ссылки", "Invite co-authors": "Пригласить соавторов", + "Invite collaborators": "Пригласить соавторов", "Invite experts": "Пригласить экспертов", "Invite to collab": "Пригласить к участию", "It does not look like url": "Это не похоже на ссылку", @@ -411,6 +421,7 @@ "Video": "Видео", "Video format not supported": "Тип видео не поддерживается", "Views": "Просмотры", + "We are working on collaborative editing of articles and in the near future you will have an amazing opportunity - to create together with your colleagues": "Мы работаем над коллаборативным редактированием статей и в ближайшем времени у вас появиться удивительная возможность - творить вместе с коллегами", "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", "We know you, please try to login": "Такой адрес почты уже зарегистрирован, попробуйте залогиниться", "We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.", @@ -434,6 +445,7 @@ "Write good articles, comment\nand it won't be so empty here": "Пишите хорошие статьи, комментируйте,\nи здесь станет не так пусто", "Write message": "Написать сообщение", "Write to us": "Напишите нам", + "Write your colleagues name or email": "Напишите имя или e-mail коллеги", "You can download multiple tracks at once in .mp3, .wav or .flac formats": "Можно загрузить сразу несколько треков в форматах .mp3, .wav или .flac", "You was successfully authorized": "Вы были успешно авторизованы", "You ll be able to participate in discussions, rate others' comments and learn about new responses": "Вы сможете участвовать в обсуждениях, оценивать комментарии других и узнавать о новых ответах", diff --git a/src/components/Editor/Panel/Panel.tsx b/src/components/Editor/Panel/Panel.tsx index 29ea82b3..b639924c 100644 --- a/src/components/Editor/Panel/Panel.tsx +++ b/src/components/Editor/Panel/Panel.tsx @@ -7,6 +7,7 @@ import Typograf from 'typograf' import { useEditorContext } from '../../../context/editor' import { useLocalize } from '../../../context/localize' import { router } from '../../../stores/router' +import { showModal } from '../../../stores/ui' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' import { Button } from '../../_shared/Button' @@ -91,7 +92,9 @@ export const Panel = (props: Props) => {

    - {t('Invite co-authors')} + showModal('inviteCoAuthors')}> + {t('Invite co-authors')} +

    { + ) } diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 2818ccbd..99e0b6a5 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -225,23 +225,12 @@ export const PublishSettings = (props: Props) => {

    - - {/*

    Соавторы

    */} - {/*

    У каждого соавтора можно добавить роль

    */} - {/*
    */} - {/*
    */} - {/* */} - {/* */} - {/*
    */} - {/* */} - {/*
    */} - - {/*
    */} - {/*
    Михаил Драбкин
    */} - {/*
    */} - {/* */} - {/*
    */} - {/*
    */} +

    {t('Collaborators')}

    + + +
    + + changeSearchParams({ + mode: 'login', + }) + } + > + {t('Cancel')} + +
    + + + + +
    {t('Password updated!')}
    +
    {t('You can now login using your new password')}
    +
    + +
    +
    + + ) +} diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index 0a7e11fc..4cceb081 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router' import { hideModal } from '../../../stores/ui' import { ApiError } from '../../../utils/apiClient' import { validateEmail } from '../../../utils/validateEmail' -import { Icon } from '../../_shared/Icon' import { AuthModalHeader } from './AuthModalHeader' +import { PasswordField } from './PasswordField' import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' @@ -35,7 +35,6 @@ export const LoginForm = () => { // TODO: better solution for interactive error messages const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [isLinkSent, setIsLinkSent] = createSignal(false) - const [showPassword, setShowPassword] = createSignal(false) const authFormRef: { current: HTMLFormElement } = { current: null } @@ -166,31 +165,7 @@ export const LoginForm = () => { -
    - handlePasswordInput(event.currentTarget.value)} - /> - - - -
    {validationErrors().password}
    -
    -
    + handlePasswordInput(value)} />
    diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss new file mode 100644 index 00000000..7faae657 --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.module.scss @@ -0,0 +1,46 @@ +.PassportField { + .passwordToggle { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + } + + .passwordToggleIcon { + height: 1.6em; + display: inline-block; + margin-right: 0.2em; + max-width: 1.4em; + max-height: 1.4em; + transition: filter 0.2s; + vertical-align: middle; + } + + .validationError { + position: absolute; + top: 100%; + font-size: 12px; + line-height: 16px; + margin-top: 0.3em; + + &.registerPassword { + margin-bottom: -32px; + } + + /* Red/500 */ + color: #d00820; + + a { + color: #d00820; + border-color: #d00820; + + &:hover { + color: var(--default-color-invert); + border-color: var(--background-color-invert); + } + } + } +} diff --git a/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx new file mode 100644 index 00000000..6dab7ef8 --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/PasswordField.tsx @@ -0,0 +1,88 @@ +import { clsx } from 'clsx' +import { createEffect, createSignal, JSX, on, Show } from 'solid-js' + +import { useLocalize } from '../../../../context/localize' +import { resetSortedArticles } from '../../../../stores/zine/articles' +import { Icon } from '../../../_shared/Icon' + +import styles from './PasswordField.module.scss' + +type Props = { + class?: string + errorMessage?: (error: string) => void + onInput: (value: string) => void +} + +export const PasswordField = (props: Props) => { + const { t } = useLocalize() + const [showPassword, setShowPassword] = createSignal(false) + const [error, setError] = createSignal() + + const validatePassword = (passwordToCheck) => { + const minLength = 8 + const hasNumber = /\d/ + const hasSpecial = /[!#$%&*@^]/ + + if (passwordToCheck.length < minLength) { + return t('Password should be at least 8 characters') + } + if (!hasNumber.test(passwordToCheck)) { + return t('Password should contain at least one number') + } + if (!hasSpecial.test(passwordToCheck)) { + return t('Password should contain at least one special character: !@#$%^&*') + } + + return null + } + + const handleInputChange = (value) => { + props.onInput(value) + const errorValue = validatePassword(value) + if (errorValue) { + setError(errorValue) + } else { + setError() + } + } + + createEffect( + on( + () => error(), + () => { + props.errorMessage(error()) + }, + { defer: true }, + ), + ) + + return ( +
    +
    + handleInputChange(event.currentTarget.value)} + /> + + + +
    {error()}
    +
    +
    +
    + ) +} diff --git a/src/components/Nav/AuthModal/PasswordField/index.ts b/src/components/Nav/AuthModal/PasswordField/index.ts new file mode 100644 index 00000000..d743619c --- /dev/null +++ b/src/components/Nav/AuthModal/PasswordField/index.ts @@ -0,0 +1 @@ +export { PasswordField } from './PasswordField' diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 7f642d3e..3b72303f 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -11,9 +11,9 @@ import { useRouter } from '../../../stores/router' import { hideModal } from '../../../stores/ui' import { ApiError } from '../../../utils/apiClient' import { validateEmail } from '../../../utils/validateEmail' -import { Icon } from '../../_shared/Icon' import { AuthModalHeader } from './AuthModalHeader' +import { PasswordField } from './PasswordField' import { email, setEmail } from './sharedLogic' import { SocialProviders } from './SocialProviders' @@ -40,9 +40,9 @@ export const RegisterForm = () => { const [fullName, setFullName] = createSignal('') const [password, setPassword] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) - const [showPassword, setShowPassword] = createSignal(false) const [isSuccess, setIsSuccess] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + const [passwordError, setPasswordError] = createSignal() const authFormRef: { current: HTMLFormElement } = { current: null } @@ -52,37 +52,15 @@ export const RegisterForm = () => { } } - function isValidPassword(passwordToCheck) { - const minLength = 8 - const hasNumber = /\d/ - const hasSpecial = /[!#$%&*@^]/ - - if (passwordToCheck.length < minLength) { - return t('Password should be at least 8 characters') - } - if (!hasNumber.test(passwordToCheck)) { - return t('Password should contain at least one number') - } - if (!hasSpecial.test(passwordToCheck)) { - return t('Password should contain at least one special character: !@#$%^&*') - } - return null - } - - const handlePasswordInput = (newPassword: string) => { - setPassword(newPassword) - } - - const handleNameInput = (newPasswordCopy: string) => { - setFullName(newPasswordCopy) + const handleNameInput = (newName: string) => { + setFullName(newName) } const handleSubmit = async (event: Event) => { event.preventDefault() - const passwordError = isValidPassword(password()) - if (passwordError) { - setValidationErrors((errors) => ({ ...errors, password: passwordError })) + if (passwordError()) { + setValidationErrors((errors) => ({ ...errors, password: passwordError() })) } else { setValidationErrors(({ password: _notNeeded, ...rest }) => rest) } @@ -198,48 +176,17 @@ export const RegisterForm = () => {
    -
    - handlePasswordInput(event.currentTarget.value)} - /> - - - -
    - {validationErrors().password} -
    -
    -
    + setPasswordError(err)} + onInput={(value) => setPassword(value)} + />
    - - {/*TODO: server sort*/} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} - -
    -
    -
    - {`${t('Show')} `} - {t('All posts')} -
    -
    + +
    +
    +
    +
      +
    • + +
    • + {/*TODO: server sort*/} + {/*
    • */} + {/* */} + {/*
    • */} + {/*
    • */} + {/* */} + {/*
    • */} + {/*
    • */} + {/* */} + {/*
    • */} +
    +
    +
    +
    + {`${t('Show')} `} + {t('All posts')}
    +
    +
    - - + + - + - + - + - - + + - 15}> - - - - + 15}> + + + + - - {(page) => ( - <> - - - - - )} - + + {(page) => ( + <> + + + + + )} + - -

    - -

    -
    - + +

    + +

    ) diff --git a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx index 9df151cf..df4d44be 100644 --- a/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ArticleCardSwiper.tsx @@ -5,6 +5,7 @@ import SwiperCore, { Manipulation, Navigation, Pagination } from 'swiper' import { Shout } from '../../../graphql/types.gen' import { ArticleCard } from '../../Feed/ArticleCard' import { Icon } from '../Icon' +import { ShowOnlyOnClient } from '../ShowOnlyOnClient' import { SwiperRef } from './swiper' @@ -25,65 +26,67 @@ export const ArticleCardSwiper = (props: Props) => { }) return ( -
    - -

    {props.title}

    -
    -
    - 0}> -
    - (mainSwipeRef.current = el)} - centered-slides={true} - observer={true} - space-between={10} - breakpoints={{ - 576: { spaceBetween: 20, slidesPerView: 1.5 }, - 992: { spaceBetween: 52, slidesPerView: 1.5 }, - }} - round-lengths={true} - loop={true} - speed={800} - autoplay={{ - disableOnInteraction: false, - delay: 6000, - pauseOnMouseEnter: true, - }} - > - - {(slide, index) => ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - - - - )} - - -
    mainSwipeRef.current.swiper.slidePrev()} - > - -
    -
    mainSwipeRef.current.swiper.slideNext()} - > - -
    -
    + +
    + +

    {props.title}

    +
    + 0}> +
    + (mainSwipeRef.current = el)} + centered-slides={true} + observer={true} + space-between={10} + breakpoints={{ + 576: { spaceBetween: 20, slidesPerView: 1.5 }, + 992: { spaceBetween: 52, slidesPerView: 1.5 }, + }} + round-lengths={true} + loop={true} + speed={800} + autoplay={{ + disableOnInteraction: false, + delay: 6000, + pauseOnMouseEnter: true, + }} + > + + {(slide, index) => ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + + + + )} + + +
    mainSwipeRef.current.swiper.slidePrev()} + > + +
    +
    mainSwipeRef.current.swiper.slideNext()} + > + +
    +
    +
    +
    -
    + ) } diff --git a/src/pages/topic.page.server.ts b/src/pages/topic.page.server.ts index 30cb8bae..82a35137 100644 --- a/src/pages/topic.page.server.ts +++ b/src/pages/topic.page.server.ts @@ -3,6 +3,7 @@ import type { PageContext } from '../renderer/types' import { render } from 'vike/abort' +import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Topic' import { apiClient } from '../utils/apiClient' export const onBeforeRender = async (pageContext: PageContext) => { @@ -14,7 +15,12 @@ export const onBeforeRender = async (pageContext: PageContext) => { throw render(404) } - const pageProps: PageProps = { topic, seo: { title: topic.title } } + const topicShouts = await apiClient.getShouts({ + filters: { topic: topic.slug }, + limit: PRERENDERED_ARTICLES_COUNT, + }) + + const pageProps: PageProps = { topic, topicShouts, seo: { title: topic.title } } return { pageContext: { diff --git a/src/pages/topic.page.tsx b/src/pages/topic.page.tsx index 574eb12a..573a3605 100644 --- a/src/pages/topic.page.tsx +++ b/src/pages/topic.page.tsx @@ -1,7 +1,8 @@ import type { PageProps } from './types' -import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from 'solid-js' +import { Loading } from '../components/_shared/Loading' import { PageLayout } from '../components/_shared/PageLayout' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic' import { ReactionsProvider } from '../context/reactions' @@ -16,7 +17,7 @@ export const TopicPage = (props: PageProps) => { const [isLoaded, setIsLoaded] = createSignal( Boolean(props.topicShouts) && Boolean(props.topic) && props.topic.slug === slug(), ) - const [pageTitle, setPageTitle] = createSignal() + const preload = () => Promise.all([ loadShouts({ filters: { topic: slug() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }), @@ -51,15 +52,15 @@ export const TopicPage = (props: PageProps) => { const usePrerenderedData = props.topic?.slug === slug() return ( - + - setPageTitle(title)} - isLoaded={isLoaded()} - topic={usePrerenderedData ? props.topic : null} - shouts={usePrerenderedData ? props.topicShouts : null} - topicSlug={slug()} - /> + }> + + ) From dd4dc967bb84a0a175434b1dc9a17695d49b12cd Mon Sep 17 00:00:00 2001 From: Arkadzi Rakouski Date: Thu, 21 Dec 2023 16:56:19 +0300 Subject: [PATCH 7/7] upgrade lightbox (#342) * upgrade lightbox * Draggable image [wip] * Resolve conversation * remove transition * remove transition only for move --------- Co-authored-by: ilya-bkv --- .../_shared/Lightbox/Lightbox.module.scss | 28 ++++++ src/components/_shared/Lightbox/Lightbox.tsx | 89 +++++++++++++++++-- .../_shared/SolidSwiper/ImageSwiper.tsx | 26 +++++- .../_shared/SolidSwiper/Swiper.module.scss | 1 + 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/components/_shared/Lightbox/Lightbox.module.scss b/src/components/_shared/Lightbox/Lightbox.module.scss index 6f99e359..de7910d2 100644 --- a/src/components/_shared/Lightbox/Lightbox.module.scss +++ b/src/components/_shared/Lightbox/Lightbox.module.scss @@ -10,6 +10,9 @@ justify-content: center; z-index: 10000; + animation: 300ms fadeIn; + animation-fill-mode: forwards; + .image { max-width: 90%; max-height: 80%; @@ -72,3 +75,28 @@ } } } + +.fadeOut { + animation: 300ms fadeOut; + animation-fill-mode: backwards; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} diff --git a/src/components/_shared/Lightbox/Lightbox.tsx b/src/components/_shared/Lightbox/Lightbox.tsx index 56092245..89c7d94b 100644 --- a/src/components/_shared/Lightbox/Lightbox.tsx +++ b/src/components/_shared/Lightbox/Lightbox.tsx @@ -1,5 +1,5 @@ import { clsx } from 'clsx' -import { createSignal } from 'solid-js' +import { createMemo, createSignal, onCleanup } from 'solid-js' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { Icon } from '../Icon' @@ -8,40 +8,109 @@ import styles from './Lightbox.module.scss' type Props = { class?: string + imageAlt?: string image: string onClose: () => void } const ZOOM_STEP = 1.08 +const TRANSITION_SPEED = 300 + export const Lightbox = (props: Props) => { const [zoomLevel, setZoomLevel] = createSignal(1) + const [translateX, setTranslateX] = createSignal(0) + const [translateY, setTranslateY] = createSignal(0) + const [transitionEnabled, setTransitionEnabled] = createSignal(false) + + const lightboxRef: { + current: HTMLElement + } = { + current: null, + } const closeLightbox = () => { - props.onClose() + lightboxRef.current?.classList.add(styles.fadeOut) + + setTimeout(() => { + props.onClose() + }, 300) } const zoomIn = (event) => { event.stopPropagation() + setTransitionEnabled(true) setZoomLevel(zoomLevel() * ZOOM_STEP) + setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED) } + const zoomOut = (event) => { event.stopPropagation() + setTransitionEnabled(true) setZoomLevel(zoomLevel() / ZOOM_STEP) + setTimeout(() => setTransitionEnabled(false), TRANSITION_SPEED) } + const zoomReset = (event) => { event.stopPropagation() setZoomLevel(1) } - const lightboxStyle = () => ({ - transform: `scale(${zoomLevel()})`, - transition: 'transform 0.3s ease', - }) + const handleWheelZoom = (event) => { + event.preventDefault() + + let scale = zoomLevel() + + scale += event.deltaY * -0.01 + + scale = Math.min(Math.max(0.125, scale), 4) + + setZoomLevel(scale * ZOOM_STEP) + } useEscKeyDownHandler(closeLightbox) + let startX: number = 0 + let startY: number = 0 + let isDragging: boolean = false + + const onMouseDown: (event: MouseEvent) => void = (event) => { + startX = event.clientX - translateX() + startY = event.clientY - translateY() + isDragging = true + event.preventDefault() + } + + const onMouseMove: (event: MouseEvent) => void = (event) => { + if (isDragging) { + setTranslateX(event.clientX - startX) + setTranslateY(event.clientY - startY) + } + } + + const onMouseUp: () => void = () => { + isDragging = false + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + + onCleanup(() => { + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + }) + + const lightboxStyle = createMemo(() => ({ + transform: `translate(${translateX()}px, ${translateY()}px) scale(${zoomLevel()})`, + transition: transitionEnabled() ? `transform ${TRANSITION_SPEED}ms ease-in-out` : '', + cursor: 'grab', + })) + return ( -
    +
    (lightboxRef.current = el)} + > @@ -59,9 +128,11 @@ export const Lightbox = (props: Props) => { {''} event.stopPropagation()} + onWheel={handleWheelZoom} + style={lightboxStyle()} + onMouseDown={onMouseDown} />
    ) diff --git a/src/components/_shared/SolidSwiper/ImageSwiper.tsx b/src/components/_shared/SolidSwiper/ImageSwiper.tsx index 93864056..5364e47b 100644 --- a/src/components/_shared/SolidSwiper/ImageSwiper.tsx +++ b/src/components/_shared/SolidSwiper/ImageSwiper.tsx @@ -7,6 +7,7 @@ import { MediaItem } from '../../../pages/types' import { getImageUrl } from '../../../utils/getImageUrl' import { Icon } from '../Icon' import { Image } from '../Image' +import { Lightbox } from '../Lightbox' import { SwiperRef } from './swiper' @@ -23,12 +24,14 @@ type Props = { const MIN_WIDTH = 540 export const ImageSwiper = (props: Props) => { - const [slideIndex, setSlideIndex] = createSignal(0) - const [isMobileView, setIsMobileView] = createSignal(false) const mainSwipeRef: { current: SwiperRef } = { current: null } const thumbSwipeRef: { current: SwiperRef } = { current: null } const swiperMainContainer: { current: HTMLDivElement } = { current: null } + const [slideIndex, setSlideIndex] = createSignal(0) + const [isMobileView, setIsMobileView] = createSignal(false) + const [selectedImage, setSelectedImage] = createSignal('') + const handleSlideChange = () => { thumbSwipeRef.current.swiper.slideTo(mainSwipeRef.current.swiper.activeIndex) setSlideIndex(mainSwipeRef.current.swiper.activeIndex) @@ -76,6 +79,19 @@ export const ImageSwiper = (props: Props) => { }) }) + const openLightbox = (image) => { + setSelectedImage(image) + } + const handleLightboxClose = () => { + setSelectedImage() + } + + const handleImageClick = (event) => { + const src = event.target.src + + openLightbox(getImageUrl(src)) + } + return (
    (swiperMainContainer.current = el)}> @@ -94,7 +110,7 @@ export const ImageSwiper = (props: Props) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -
    +
    {slide.title}
    @@ -178,6 +194,10 @@ export const ImageSwiper = (props: Props) => {
    + + + +
    ) } diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss index 4928695e..f4dbccf1 100644 --- a/src/components/_shared/SolidSwiper/Swiper.module.scss +++ b/src/components/_shared/SolidSwiper/Swiper.module.scss @@ -188,6 +188,7 @@ img { max-height: 100%; width: auto; + cursor: zoom-in; } }