diff --git a/public/icons/lightning.svg b/public/icons/lightning.svg new file mode 100644 index 00000000..11ddae16 --- /dev/null +++ b/public/icons/lightning.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 2911d277..8090e2f2 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -289,6 +289,7 @@ "PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}", "Publish Album": "Publish Album", "Publish Settings": "Publish Settings", + "Published": "Published", "Punchline": "Punchline", "Quit": "Quit", "Quote": "Quote", @@ -333,6 +334,7 @@ "Something went wrong, please try again": "Something went wrong, please try again", "Song lyrics": "Song lyrics...", "Song title": "Song title", + "Soon": "Скоро", "Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one", "Special Projects": "Special Projects", "Special projects": "Special projects", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 1248ac72..2732e4f8 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -307,6 +307,7 @@ "Publish": "Опубликовать", "Publish Album": "Опубликовать альбом", "Publish Settings": "Настройки публикации", + "Published": "Опубликованные", "Punchline": "Панчлайн", "Quit": "Выйти", "Quote": "Цитата", @@ -354,6 +355,7 @@ "Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз", "Song lyrics": "Текст песни...", "Song title": "Название песни", + "Soon": "Скоро", "Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой", "Special Projects": "Спецпроекты", "Special projects": "Спецпроекты", diff --git a/src/components/App.tsx b/src/components/App.tsx index f42f704c..dfc2be5d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,6 +8,7 @@ import { ConfirmProvider } from '../context/confirm' import { ConnectProvider } from '../context/connect' import { EditorProvider } from '../context/editor' import { LocalizeProvider } from '../context/localize' +import { MediaQueryProvider } from '../context/mediaQuery' import { NotificationsProvider } from '../context/notifications' import { SessionProvider } from '../context/session' import { SnackbarProvider } from '../context/snackbar' @@ -116,19 +117,21 @@ export const App = (props: Props) => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + ) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 7998265e..1fe89808 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -509,7 +509,6 @@ export const FullArticle = (props: Props) => { title={props.article.title} description={description} imageUrl={props.article.cover} - shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })} trigger={ - - -
  • - -
  • -
    -
  • - -
  • - -
  • - -
  • -
    -
  • - -
  • - -
  • - -
  • -
    -
  • - -
  • - - - ) -} diff --git a/src/components/Feed/FeedArticlePopup/FeedArticlePopup.module.scss b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.module.scss new file mode 100644 index 00000000..c983fe14 --- /dev/null +++ b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.module.scss @@ -0,0 +1,48 @@ +.feedArticlePopup { + box-shadow: none !important; + border: 1px solid rgb(0 0 0 / 15%); + border-radius: 1.6rem; + padding: 0 !important; + text-align: left; + overflow: hidden; + + @include media-breakpoint-down(md) { + left: auto !important; + right: 0; + transform: none !important; + } + + .actionList { + & > li { + margin-bottom: 0 !important; + } + + .action { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; + padding: 8px 16px; + font-size: inherit; + font-weight: 500; + text-align: left; + white-space: nowrap; + + &.soon { + color: var(--black-300); + } + + &:hover { + background: var(--black-500); + color: var(--black-50) !important; + } + } + + li:first-child .action { + padding-top: 16px; + } + li:last-child .action { + padding-bottom: 16px; + } + } +} diff --git a/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx new file mode 100644 index 00000000..3e237307 --- /dev/null +++ b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx @@ -0,0 +1,92 @@ +import type { PopupProps } from '../../_shared/Popup' + +import { clsx } from 'clsx' +import { createEffect, createSignal, Show } from 'solid-js' + +import { useLocalize } from '../../../context/localize' +import { showModal } from '../../../stores/ui' +import { InviteCoAuthorsModal } from '../../_shared/InviteCoAuthorsModal' +import { Popup } from '../../_shared/Popup' +import { SoonChip } from '../../_shared/SoonChip' + +import styles from './FeedArticlePopup.module.scss' + +type FeedArticlePopupProps = { + title: string + imageUrl: string + isOwner: boolean + description: string +} & Omit + +export const FeedArticlePopup = (props: FeedArticlePopupProps) => { + const { t } = useLocalize() + return ( + <> + + + + + + ) +} diff --git a/src/components/Feed/FeedArticlePopup/index.ts b/src/components/Feed/FeedArticlePopup/index.ts new file mode 100644 index 00000000..35792480 --- /dev/null +++ b/src/components/Feed/FeedArticlePopup/index.ts @@ -0,0 +1 @@ +export { FeedArticlePopup } from './FeedArticlePopup' diff --git a/src/components/Nav/Header/Header.module.scss b/src/components/Nav/Header/Header.module.scss index b670f19e..018adc78 100644 --- a/src/components/Nav/Header/Header.module.scss +++ b/src/components/Nav/Header/Header.module.scss @@ -5,7 +5,7 @@ margin-bottom: 2.2rem; position: absolute; width: 100%; - z-index: 10000; + z-index: 10003; .wide-container { background: #fff; @@ -149,7 +149,7 @@ position: fixed; top: 58px; width: 100%; - z-index: 1; + z-index: 10003; li { margin-bottom: 2.4rem !important; diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index 0ee8c1a6..fb597f05 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -62,7 +62,9 @@ export const Header = (props: Props) => { const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false) - const toggleFixed = () => setFixed((oldFixed) => !oldFixed) + const toggleFixed = () => { + setFixed(!fixed()) + } const tag = (topic: Topic) => /[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title @@ -188,9 +190,9 @@ export const Header = (props: Props) => {
    -
    )} diff --git a/src/components/Views/Feed/Feed.module.scss b/src/components/Views/Feed/Feed.module.scss index 5c97c2ca..3099733d 100644 --- a/src/components/Views/Feed/Feed.module.scss +++ b/src/components/Views/Feed/Feed.module.scss @@ -1,6 +1,6 @@ .feedFilter { @include media-breakpoint-down(md) { - margin-right: 4rem !important; + margin-right: 1rem !important; } } @@ -195,15 +195,29 @@ justify-content: space-between; align-items: center; margin-bottom: 4rem; + @include media-breakpoint-down(sm) { + flex-direction: column-reverse; + align-items: flex-start; + gap: 1rem; + } .feedFilter { margin-top: 0; margin-bottom: 0; + min-width: 300px; & > li { margin-bottom: 0; } } + + .dropdowns { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 1rem; + justify-content: center; + } } .periodSwitcher { diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 1edb897d..a717cf12 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -31,15 +31,22 @@ export const FEED_PAGE_SIZE = 20 const UNRATED_ARTICLES_COUNT = 5 type FeedPeriod = 'week' | 'month' | 'year' +type VisibilityMode = 'all' | 'community' | 'public' type PeriodItem = { value: FeedPeriod title: string } +type VisibilityItem = { + value: VisibilityMode + title: string +} + type FeedSearchParams = { by: 'publish_date' | 'rating' | 'last_comment' period: FeedPeriod + visibility: VisibilityMode } const getOrderBy = (by: FeedSearchParams['by']) => { @@ -85,6 +92,7 @@ export const FeedView = (props: Props) => { const { t } = useLocalize() const monthPeriod: PeriodItem = { value: 'month', title: t('This month') } + const visibilityAll = { value: 'public', title: t('All') } const periods: PeriodItem[] = [ { value: 'week', title: t('This week') }, @@ -92,6 +100,11 @@ export const FeedView = (props: Props) => { { value: 'year', title: t('This year') }, ] + const visibilities: VisibilityItem[] = [ + { value: 'community', title: t('All') }, + { value: 'public', title: t('Published') }, + ] + const { page, searchParams, changeSearchParams } = useRouter() const [isLoading, setIsLoading] = createSignal(false) const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false) @@ -105,14 +118,20 @@ export const FeedView = (props: Props) => { const currentPeriod = createMemo(() => { const period = periods.find((p) => p.value === searchParams().period) - if (!period) { return monthPeriod } - return period }) + const currentVisibility = createMemo(() => { + const visibility = visibilities.find((v) => v.value === searchParams().visibility) + if (!visibility) { + return visibilityAll + } + return visibility + }) + const { actions: { loadReactionsBy }, } = useReactions() @@ -130,7 +149,7 @@ export const FeedView = (props: Props) => { onMount(() => { loadMore() // eslint-disable-next-line promise/catch-or-return - Promise.all([loadTopComments()]).finally(() => setIsRightColumnLoaded(true)) + Promise.all([loadUnratedArticles(), loadTopComments()]).finally(() => setIsRightColumnLoaded(true)) }) const { session } = useSession() @@ -142,7 +161,7 @@ export const FeedView = (props: Props) => { createEffect( on( - () => page().route + searchParams().by + searchParams().period, + () => page().route + searchParams().by + searchParams().period + searchParams().visibility, () => { resetSortedArticles() loadMore() @@ -158,16 +177,19 @@ export const FeedView = (props: Props) => { } const orderBy = getOrderBy(searchParams().by) - if (orderBy) { options.order_by = orderBy } + const visibilityMode = searchParams().visibility + if (visibilityMode && visibilityMode !== 'all') { + options.filters = { ...options.filters, published: visibilityMode === 'public' } + } + if (searchParams().by && searchParams().by !== 'publish_date') { const period = searchParams().period || 'month' options.filters = { after: getFromDate(period) } } - return props.loadShouts(options) } @@ -242,16 +264,24 @@ export const FeedView = (props: Props) => { - -
    +
    + changeSearchParams({ period: period.value })} + onChange={(period: PeriodItem) => changeSearchParams({ period: period.value })} /> -
    - + + + changeSearchParams({ visibility: visibility.value }) + } + /> +
    }> diff --git a/src/components/Views/Home.module.scss b/src/components/Views/Home.module.scss index 0f44ea68..b4a4ae37 100644 --- a/src/components/Views/Home.module.scss +++ b/src/components/Views/Home.module.scss @@ -9,7 +9,6 @@ font-size: 40px; font-weight: 700; line-height: 44px; - text-transform: capitalize; } .randomTopicHeaderLink { diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 2d8121c8..dfcb0d6b 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -13,6 +13,7 @@ import { } from '../../stores/zine/articles' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopicsStore } from '../../stores/zine/topics' +import { capitalize } from '../../utils/capitalize' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { splitToPages } from '../../utils/splitToPages' import { Icon } from '../_shared/Icon' @@ -134,7 +135,7 @@ export const HomeView = (props: Props) => { articles={randomTopicArticles()} header={
    -
    {randomTopic().title}
    +
    {capitalize(randomTopic().title, true)}
    (props: Props) +
    {props.currentOption.title}{' '} { +type Props = { + title?: string +} +export const InviteCoAuthorsModal = (props: Props) => { const { t } = useLocalize() return ( -

    {t('Invite collaborators')}

    +

    {props.title || t('Invite collaborators')}

    {}} />
    ) diff --git a/src/components/_shared/Popup/Popup.tsx b/src/components/_shared/Popup/Popup.tsx index f9cb0902..c0b04aec 100644 --- a/src/components/_shared/Popup/Popup.tsx +++ b/src/components/_shared/Popup/Popup.tsx @@ -36,9 +36,7 @@ export const Popup = (props: PopupProps) => { setIsVisible(false) }, }) - const toggle = () => setIsVisible((oldVisible) => !oldVisible) - return ( (containerRef.current = el)}> diff --git a/src/components/_shared/SearchField/SearchField.module.scss b/src/components/_shared/SearchField/SearchField.module.scss index 03811331..5f0ff538 100644 --- a/src/components/_shared/SearchField/SearchField.module.scss +++ b/src/components/_shared/SearchField/SearchField.module.scss @@ -2,6 +2,7 @@ display: flex; justify-content: flex-end; position: relative; + min-width: 100px; &.bordered { border: 2px solid var(--black-100); diff --git a/src/components/_shared/SoonChip/SoonChip.module.scss b/src/components/_shared/SoonChip/SoonChip.module.scss new file mode 100644 index 00000000..7a96f725 --- /dev/null +++ b/src/components/_shared/SoonChip/SoonChip.module.scss @@ -0,0 +1,23 @@ +.SoonChip { + @include font-size(1.2rem); + + display: inline-flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + height: 22px; + padding: 2px 7px 2px 3px; + gap: -1px; + margin-left: 0.5rem; + border-radius: 8px; + background: var(--black-500); + color: var(--black-50); + font-weight: 700; + letter-spacing: 0.036px; + line-height: 1; + + .icon { + width: 16px; + height: 16px; + } +} diff --git a/src/components/_shared/SoonChip/SoonChip.tsx b/src/components/_shared/SoonChip/SoonChip.tsx new file mode 100644 index 00000000..a0acc2f7 --- /dev/null +++ b/src/components/_shared/SoonChip/SoonChip.tsx @@ -0,0 +1,20 @@ +import { clsx } from 'clsx' + +import { useLocalize } from '../../../context/localize' +import { Icon } from '../Icon' + +import styles from './SoonChip.module.scss' + +type Props = { + class?: string +} + +export const SoonChip = (props: Props) => { + const { t } = useLocalize() + return ( +
    + + {t('Soon')} +
    + ) +} diff --git a/src/components/_shared/SoonChip/index.ts b/src/components/_shared/SoonChip/index.ts new file mode 100644 index 00000000..ac55e9fa --- /dev/null +++ b/src/components/_shared/SoonChip/index.ts @@ -0,0 +1 @@ +export { SoonChip } from './SoonChip' diff --git a/src/context/mediaQuery.tsx b/src/context/mediaQuery.tsx new file mode 100644 index 00000000..37a444e5 --- /dev/null +++ b/src/context/mediaQuery.tsx @@ -0,0 +1,31 @@ +import type { JSX } from 'solid-js' + +import { createBreakpoints } from '@solid-primitives/media' +import { createContext, useContext } from 'solid-js' + +const breakpoints = { + xs: '0', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + xxl: '1400px', +} + +type MediaQueryContextType = { + mediaMatches: ReturnType +} + +const MediaQueryContext = createContext() + +export function useMediaQuery() { + return useContext(MediaQueryContext) +} + +export const MediaQueryProvider = (props: { children: JSX.Element }) => { + const mediaMatches = createBreakpoints(breakpoints) + + const value: MediaQueryContextType = { mediaMatches } + + return {props.children} +} diff --git a/src/pages/topic.page.server.ts b/src/pages/topic.page.server.ts index d495aa8b..e19d70fa 100644 --- a/src/pages/topic.page.server.ts +++ b/src/pages/topic.page.server.ts @@ -16,7 +16,7 @@ export const onBeforeRender = async (pageContext: PageContext) => { } const topicShouts = await apiClient.getShouts({ - filters: { topic: topic.slug }, + filters: { topic: topic.slug, visibility: 'public' }, limit: PRERENDERED_ARTICLES_COUNT, }) diff --git a/src/pages/topic.page.tsx b/src/pages/topic.page.tsx index 573a3605..424e927b 100644 --- a/src/pages/topic.page.tsx +++ b/src/pages/topic.page.tsx @@ -20,7 +20,11 @@ export const TopicPage = (props: PageProps) => { const preload = () => Promise.all([ - loadShouts({ filters: { topic: slug() }, limit: PRERENDERED_ARTICLES_COUNT, offset: 0 }), + loadShouts({ + filters: { topic: slug(), visibility: 'public' }, + limit: PRERENDERED_ARTICLES_COUNT, + offset: 0, + }), loadTopic({ slug: slug() }), ])