Merge remote-tracking branch 'hub/main' into feature/sse-connect
Some checks failed
deploy / test (push) Failing after 1m7s

This commit is contained in:
Untone 2024-01-04 10:14:57 +03:00
commit 3af0c47738
40 changed files with 510 additions and 288 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.89589 13.4965C6.77089 13.6545 6.63373 13.7326 6.48269 13.7309C6.33339 13.7291 6.21533 13.6684 6.132 13.5468C6.04867 13.4271 6.04172 13.2691 6.11637 13.0712L7.61117 9.19789H4.76915C4.65283 9.19789 4.55561 9.16143 4.47575 9.08504C4.39589 9.01039 4.35596 8.91491 4.35596 8.79859C4.35596 8.68227 4.40283 8.56421 4.49832 8.44442L9.10422 2.50345C9.22922 2.34546 9.36637 2.26733 9.51742 2.26907C9.66672 2.27081 9.78478 2.33157 9.86811 2.4531C9.95144 2.57289 9.95839 2.73088 9.88373 2.92879L8.38894 6.80206H11.231C11.3473 6.80206 11.4445 6.83852 11.5244 6.9149C11.6042 6.98956 11.6442 7.08504 11.6442 7.20136C11.6442 7.31768 11.5973 7.43574 11.5018 7.55553L6.89589 13.4965Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -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",

View File

@ -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": "Спецпроекты",

View File

@ -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) => {
<MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<LocalizeProvider>
<SnackbarProvider>
<ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}>
<ConnectProvider>
<NotificationsProvider>
<EditorProvider>
<Dynamic component={pageComponent()} {...props} />
</EditorProvider>
</NotificationsProvider>
</ConnectProvider>
</SessionProvider>
</ConfirmProvider>
</SnackbarProvider>
<MediaQueryProvider>
<SnackbarProvider>
<ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}>
<ConnectProvider>
<NotificationsProvider>
<EditorProvider>
<Dynamic component={pageComponent()} {...props} />
</EditorProvider>
</NotificationsProvider>
</ConnectProvider>
</SessionProvider>
</ConfirmProvider>
</SnackbarProvider>
</MediaQueryProvider>
</LocalizeProvider>
</MetaProvider>
)

View File

@ -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={
<button>
<Icon name="ellipsis" class={clsx(styles.icon)} />

View File

@ -15,7 +15,7 @@ type SharePopupProps = {
shareUrl?: string
imageUrl: string
description: string
isVisible?: (value: boolean) => void
onVisibilityChange?: (value: boolean) => void
} & Omit<PopupProps, 'children'>
export const getShareUrl = (params: { pathname?: string } = {}) => {
@ -32,8 +32,8 @@ export const SharePopup = (props: SharePopupProps) => {
} = useSnackbar()
createEffect(() => {
if (props.isVisible) {
props.isVisible(isVisible())
if (props.onVisibilityChange) {
props.onVisibilityChange(isVisible())
}
})

View File

@ -2,7 +2,7 @@
align-items: flex-start;
display: flex;
gap: 1rem;
margin-bottom: 3rem;
margin-bottom: 2rem;
&.nameOnly {
align-items: center;
@ -12,10 +12,6 @@
}
}
@include media-breakpoint-up(sm) {
margin-bottom: 2rem;
}
@include media-breakpoint-down(md) {
text-align: left;
}
@ -60,6 +56,7 @@
.bio {
color: var(--black-400);
font-weight: 500;
}
}

View File

@ -1,8 +1,9 @@
import { openPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { createEffect, createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router'
@ -26,7 +27,14 @@ type Props = {
nameOnly?: boolean
}
export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false)
createEffect(() => {
setIsMobileView(!mediaMatches.sm)
})
const {
author,
subscriptions,
@ -80,7 +88,7 @@ export const AuthorBadge = (props: Props) => {
<div class={styles.basicInfo}>
<Userpic
hasLink={true}
size={'M'}
size={isMobileView() ? 'M' : 'L'}
name={name()}
userpic={props.author.pic}
slug={props.author.slug}
@ -128,7 +136,7 @@ export const AuthorBadge = (props: Props) => {
fallback={
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
size="S"
value={
<Show
when={props.iconButtons}
@ -152,7 +160,7 @@ export const AuthorBadge = (props: Props) => {
>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
size="S"
value={
<Show
when={props.iconButtons}
@ -178,7 +186,7 @@ export const AuthorBadge = (props: Props) => {
<Show when={props.showMessageButton}>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
size="S"
value={props.iconButtons ? <Icon name="inbox-white" /> : t('Message')}
onClick={initChat}
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}

View File

@ -29,7 +29,6 @@ type Props = {
followers?: Author[]
following?: Array<Author | Topic>
}
export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize()
const {
@ -254,7 +253,7 @@ export const AuthorCard = (props: Props) => {
</Show>
</ShowOnlyOnClient>
<Show when={props.followers}>
<Modal variant="medium" name="followers" maxHeight>
<Modal variant="medium" isResponsive={true} name="followers" maxHeight>
<>
<h2>{t('Followers')}</h2>
<div class={styles.listWrapper}>
@ -270,7 +269,7 @@ export const AuthorCard = (props: Props) => {
</Modal>
</Show>
<Show when={props.following}>
<Modal variant="medium" name="following" maxHeight>
<Modal variant="medium" isResponsive={true} name="following" maxHeight>
<>
<h2>{t('Subscriptions')}</h2>
<ul class="view-switcher">

View File

@ -107,7 +107,7 @@
height: 0;
margin-bottom: 1.6rem;
overflow: hidden;
padding-bottom: 56.2%;
padding-bottom: 56.2%; //16:9
position: relative;
transform-origin: 50% 50%;
transition: transform 1s ease-in-out;
@ -267,6 +267,24 @@
}
}
.aspectRatio1x1 {
.shoutCardCover {
padding-bottom: 100%;
}
}
.aspectRatio4x3 {
.shoutCardCover {
padding-bottom: 75%;
}
}
.aspectRatio16x9 {
.shoutCardCover {
padding-bottom: 56.25%;
}
}
.shoutCardType {
height: 3.2rem;
position: absolute;

View File

@ -46,6 +46,7 @@ export type ArticleCardProps = {
withViewed?: boolean
noAuthorLink?: boolean
}
withAspectRatio?: boolean
desktopCoverSize?: 'XS' | 'S' | 'M' | 'L'
article: Shout
}
@ -117,6 +118,22 @@ export const ArticleCard = (props: ArticleCardProps) => {
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => {
switch (props.article.layout) {
case 'music': {
return styles.aspectRatio1x1
}
case 'image': {
return styles.aspectRatio4x3
}
case 'video':
case 'literature': {
return styles.aspectRatio16x9
}
}
}
return (
<section
class={clsx(styles.shoutCard, props.settings?.additionalClass, {
@ -132,6 +149,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardSingle]: props.settings?.isSingle,
[styles.shoutCardBeside]: props.settings?.isBeside,
[styles.shoutCardNoImage]: !props.article.cover,
[aspectRatio()]: props.withAspectRatio,
})}
>
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
@ -159,7 +177,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
</div>
</div>
</Show>
<div class={styles.shoutCardContent}>
<Show
when={
@ -331,7 +348,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
description={description}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
isVisible={(value) => setIsActionPopupActive(value)}
trigger={
<button>
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
@ -353,8 +369,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
title={title}
description={description}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
isVisible={(value) => setIsActionPopupActive(value)}
trigger={
<button>
<Icon name="ellipsis" class={clsx(styles.icon, styles.feedControlIcon)} />

View File

@ -1,25 +0,0 @@
.feedArticlePopup {
box-shadow: none !important;
border: 1px solid rgb(0 0 0 / 15%);
border-radius: 1.6rem;
padding: 1.6rem !important;
text-align: left;
@include media-breakpoint-down(md) {
left: auto !important;
right: 0;
transform: none !important;
}
button {
font-size: inherit;
font-weight: 500;
text-align: left;
white-space: nowrap;
&:hover {
background: #000;
color: #fff !important;
}
}
}

View File

@ -1,115 +0,0 @@
import type { PopupProps } from '../_shared/Popup'
import { createEffect, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup'
import styles from './FeedArticlePopup.module.scss'
type FeedArticlePopupProps = {
title: string
shareUrl?: string
imageUrl: string
isOwner: boolean
description: string
isVisible?: (value: boolean) => void
} & Omit<PopupProps, 'children'>
export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
const { t } = useLocalize()
const [isVisible, setIsVisible] = createSignal(false)
createEffect(() => {
if (props.isVisible) {
props.isVisible(isVisible())
}
})
return (
<Popup
{...props}
variant="tiny"
onVisibilityChange={(value) => setIsVisible(value)}
popupCssClass={styles.feedArticlePopup}
>
<ul class="nodash">
<li>
<button
role="button"
onClick={() => {
alert('Share')
}}
>
{t('Share')}
</button>
</li>
<Show when={!props.isOwner}>
<li>
<button
role="button"
onClick={() => {
alert('Help to edit')
}}
>
{t('Help to edit')}
</button>
</li>
</Show>
<li>
<button
role="button"
onClick={() => {
alert('Invite experts')
}}
>
{t('Invite experts')}
</button>
</li>
<Show when={!props.isOwner}>
<li>
<button
role="button"
onClick={() => {
alert('Subscribe to comments')
}}
>
{t('Subscribe to comments')}
</button>
</li>
</Show>
<li>
<button
role="button"
onClick={() => {
alert('Add to bookmarks')
}}
>
{t('Add to bookmarks')}
</button>
</li>
<Show when={!props.isOwner}>
<li>
<button
role="button"
onClick={() => {
alert('Report')
}}
>
{t('Report')}
</button>
</li>
</Show>
<li>
<button
role="button"
onClick={() => {
alert('Get notifications')
}}
>
{t('Get notifications')}
</button>
</li>
</ul>
</Popup>
)
}

View File

@ -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;
}
}
}

View File

@ -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<PopupProps, 'children'>
export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
const { t } = useLocalize()
return (
<>
<Popup {...props} variant="tiny" popupCssClass={styles.feedArticlePopup}>
<ul class={clsx('nodash', styles.actionList)}>
<Show when={!props.isOwner}>
<li>
<button
class={styles.action}
role="button"
onClick={() => {
alert('Help to edit')
}}
>
{t('Help to edit')}
</button>
</li>
</Show>
<li>
<button
class={styles.action}
role="button"
onClick={() => {
showModal('inviteCoAuthors')
}}
>
{t('Invite experts')}
</button>
</li>
<Show when={!props.isOwner}>
<li>
<button class={clsx(styles.action, styles.soon)} role="button">
{t('Subscribe to comments')} <SoonChip />
</button>
</li>
</Show>
<li>
<button class={clsx(styles.action, styles.soon)} role="button">
{t('Add to bookmarks')} <SoonChip />
</button>
</li>
{/*<Show when={!props.isOwner}>*/}
{/* <li>*/}
{/* <button*/}
{/* class={styles.action}*/}
{/* role="button"*/}
{/* onClick={() => {*/}
{/* alert('Report')*/}
{/* }}*/}
{/* >*/}
{/* {t('Report')}*/}
{/* </button>*/}
{/* </li>*/}
{/*</Show>*/}
{/*<li>*/}
{/* <button*/}
{/* class={styles.action}*/}
{/* role="button"*/}
{/* onClick={() => {*/}
{/* alert('Get notifications')*/}
{/* }}*/}
{/* >*/}
{/* {t('Get notifications')}*/}
{/* </button>*/}
{/*</li>*/}
</ul>
</Popup>
<InviteCoAuthorsModal title={t('Invite experts')} />
</>
)
}

View File

@ -0,0 +1 @@
export { FeedArticlePopup } from './FeedArticlePopup'

View File

@ -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;

View File

@ -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) => {
</Modal>
<div class={clsx(styles.mainHeaderInner, 'wide-container')}>
<nav class={clsx('row', styles.headerInner, { ['fixed']: fixed() })}>
<nav class={clsx('row', styles.headerInner, { [styles.fixed]: fixed() })}>
<div class={clsx(styles.burgerContainer, 'col-auto')}>
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
<div class={clsx(styles.burger, { [styles.fixed]: fixed() })} onClick={toggleFixed}>
<div />
</div>
</div>

View File

@ -10,11 +10,11 @@
position: fixed;
top: 0;
width: 100%;
z-index: 10002;
z-index: 10003;
}
.modal {
background: #fff;
background: var(--background-color);
max-width: 1000px;
position: relative;
@ -115,6 +115,26 @@
height: 90vh;
}
.backdrop.isMobile {
z-index: 10002;
top: 56px;
height: calc(100% - 58px);
bottom: 0;
.maxHeight {
height: 100%;
}
.container {
padding: 0;
height: 100%;
min-height: 100%;
}
.modalInner {
padding: 1rem 1rem 0;
height: 100%;
}
}
.modal-search {
background: #000;

View File

@ -4,6 +4,7 @@ import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { useMediaQuery } from '../../../context/mediaQuery'
import { router } from '../../../stores/router'
import { hideModal, useModalStore } from '../../../stores/ui'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
@ -19,12 +20,15 @@ interface Props {
noPadding?: boolean
maxHeight?: boolean
allowClose?: boolean
isResponsive?: boolean
}
export const Modal = (props: Props) => {
const { modal } = useModalStore()
const [visible, setVisible] = createSignal(false)
const allowClose = createMemo(() => props.allowClose !== false)
const [isMobileView, setIsMobileView] = createSignal(false)
const { mediaMatches } = useMediaQuery()
const handleHide = () => {
if (modal()) {
if (allowClose()) {
@ -33,7 +37,6 @@ export const Modal = (props: Props) => {
redirectPage(router, 'home')
}
}
hideModal()
}
@ -43,10 +46,21 @@ export const Modal = (props: Props) => {
setVisible(modal() === props.name)
})
createEffect(() => {
if (props.isResponsive) {
setIsMobileView(!mediaMatches.sm)
}
})
return (
<Show when={visible()}>
<div class={clsx(styles.backdrop, [styles[`modal-${props.name}`]])} onClick={handleHide}>
<div class="wide-container">
<div
class={clsx(styles.backdrop, {
[styles.isMobile]: isMobileView(),
})}
onClick={handleHide}
>
<div class={clsx('wide-container', styles.container)}>
<div
class={clsx(styles.modal, {
[styles.narrow]: props.variant === 'narrow',
@ -57,9 +71,11 @@ export const Modal = (props: Props) => {
onClick={(event) => event.stopPropagation()}
>
<div class={styles.modalInner}>{props.children}</div>
<div class={styles.close} onClick={handleHide}>
<Icon name="close" class={styles.icon} />
</div>
<Show when={!isMobileView()}>
<div class={styles.close} onClick={handleHide}>
<Icon name="close" class={styles.icon} />
</div>
</Show>
</div>
</div>
</div>

View File

@ -6,8 +6,10 @@
overflow: hidden;
position: relative;
transform: translateY(-2px);
width: 100%;
@include media-breakpoint-down(sm) {
overflow: auto;
padding: 0 divide($container-padding-x, 2);
}

View File

@ -3,14 +3,15 @@
flex-direction: row;
align-items: flex-start;
margin-bottom: 2rem;
gap: 1rem;
@include media-breakpoint-down(sm) {
flex-wrap: wrap;
margin-bottom: 3rem;
}
@include media-breakpoint-down(md) {
text-align: left;
.basicInfo {
display: flex;
flex-direction: row;
align-items: flex-start;
flex-wrap: nowrap;
flex: 1;
gap: 1rem;
}
.picture {
@ -24,7 +25,12 @@
background-position: 50% 50%;
background-repeat: no-repeat;
border: none;
margin-right: 1.2rem;
&.smallSize {
width: 32px;
height: 32px;
min-width: 32px;
}
&:hover {
background-color: var(--black-50);
@ -40,43 +46,31 @@
border: none;
display: flex;
flex: 0 calc(100% - 5.2rem);
flex-direction: column;
margin-bottom: 1rem;
@include media-breakpoint-up(sm) {
flex: 1 100%;
}
&:hover {
background: unset;
}
.title {
@include font-size(1.4rem);
font-weight: 500;
line-height: 1em;
color: var(--blue-500);
font-weight: 700;
text-transform: uppercase;
}
.description {
color: var(--black-400);
font-weight: 500;
}
}
.actions {
flex: 0 20%;
margin-left: 5.2rem;
@include media-breakpoint-up(sm) {
margin-left: 2rem;
}
@include media-breakpoint-up(md) {
flex: 1;
margin-left: auto;
padding-left: 1rem;
text-align: right;
}
display: flex;
flex-direction: row;
gap: 1rem;
}
.subscribeButton {

View File

@ -1,7 +1,8 @@
import { clsx } from 'clsx'
import { createMemo, createSignal, Show } from 'solid-js'
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { follow, unfollow } from '../../../stores/zine/common'
@ -20,8 +21,13 @@ type Props = {
export const TopicBadge = (props: Props) => {
const [isSubscribing, setIsSubscribing] = createSignal(false)
const { t, lang } = useLocalize()
const { mediaMatches } = useMediaQuery()
const [isMobileView, setIsMobileView] = createSignal(false)
const [isSubscribing, setIsSubscribing] = createSignal(false)
createEffect(() => {
setIsMobileView(!mediaMatches.sm)
})
const {
isAuthenticated,
subscriptions,
actions: { loadSubscriptions },
} = useSession()
@ -41,67 +47,68 @@ export const TopicBadge = (props: Props) => {
setIsSubscribing(false)
}
const title = () =>
lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title
return (
<div class={styles.TopicBadge}>
<a
href={`/topic/${props.topic.slug}`}
class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })}
style={
props.topic.pic && {
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`,
<div class={styles.basicInfo}>
<a
href={`/topic/${props.topic.slug}`}
class={clsx(styles.picture, {
[styles.withImage]: props.topic.pic,
[styles.smallSize]: isMobileView(),
})}
style={
props.topic.pic && {
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`,
}
}
}
/>
<a href={`/topic/${props.topic.slug}`} class={styles.info}>
<span class={styles.title}>
{lang() === 'en' ? capitalize(props.topic.slug.replaceAll('-', ' ')) : props.topic.title}
</span>
/>
<a href={`/topic/${props.topic.slug}`} class={styles.info}>
<span class={styles.title}>{title()}</span>
<Show
when={props.topic.body}
fallback={
<div class={styles.description}>
{t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
</div>
}
>
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
</Show>
</a>
</div>
<div class={styles.actions}>
<Show
when={props.topic.body}
when={!props.minimizeSubscribeButton}
fallback={
<div class={styles.description}>
{t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
</div>
<CheckButton text={t('Follow')} checked={subscribed()} onClick={() => subscribe(!subscribed)} />
}
>
<div class={clsx('text-truncate', styles.description)}>{props.topic.body}</div>
</Show>
</a>
<Show when={isAuthenticated()}>
<div class={styles.actions}>
<Show
when={!props.minimizeSubscribeButton}
when={subscribed()}
fallback={
<CheckButton
text={t('Follow')}
checked={subscribed()}
onClick={() => subscribe(!subscribed)}
<Button
variant="primary"
size="S"
value={isSubscribing() ? t('subscribing...') : t('Subscribe')}
onClick={() => subscribe(true)}
class={styles.subscribeButton}
/>
}
>
<Show
when={subscribed()}
fallback={
<Button
variant="primary"
size="S"
value={isSubscribing() ? t('subscribing...') : t('Subscribe')}
onClick={() => subscribe(true)}
class={styles.subscribeButton}
/>
}
>
<Button
onClick={() => subscribe(false)}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton}
/>
</Show>
<Button
onClick={() => subscribe(false)}
variant="bordered"
size="S"
value={t('Following')}
class={styles.subscribeButton}
/>
</Show>
</div>
</Show>
</Show>
</div>
</div>
)
}

View File

@ -75,4 +75,9 @@
.viewSwitcher {
margin-bottom: 2rem;
width: 100%;
@include media-breakpoint-down(sm) {
overflow-x: auto;
}
}

View File

@ -143,7 +143,6 @@ export const Expo = (props: Props) => {
const handleLoadMoreClick = () => {
loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE)
}
return (
<div class={styles.Expo}>
<Show when={sortedArticles().length > 0} fallback={<Loading />}>
@ -206,6 +205,7 @@ export const Expo = (props: Props) => {
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
@ -220,6 +220,7 @@ export const Expo = (props: Props) => {
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}
@ -236,6 +237,7 @@ export const Expo = (props: Props) => {
article={shout}
settings={{ nodate: true, nosubtitle: true, noAuthorLink: true }}
desktopCoverSize="XS"
withAspectRatio={true}
/>
</div>
)}

View File

@ -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 {

View File

@ -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<FeedSearchParams>()
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) => {
</span>
</li>
</ul>
<Show when={searchParams().by && searchParams().by !== 'publish_date'}>
<div>
<div class={styles.dropdowns}>
<Show when={searchParams().by && searchParams().by !== 'publish_date'}>
<DropDown
options={periods}
currentOption={currentPeriod()}
triggerCssClass={styles.periodSwitcher}
onChange={(period) => changeSearchParams({ period: period.value })}
onChange={(period: PeriodItem) => changeSearchParams({ period: period.value })}
/>
</div>
</Show>
</Show>
<DropDown
options={visibilities}
currentOption={currentVisibility()}
triggerCssClass={styles.periodSwitcher}
onChange={(visibility: VisibilityItem) =>
changeSearchParams({ visibility: visibility.value })
}
/>
</div>
</div>
<Show when={!isLoading()} fallback={<Loading />}>

View File

@ -9,7 +9,6 @@
font-size: 40px;
font-weight: 700;
line-height: 44px;
text-transform: capitalize;
}
.randomTopicHeaderLink {

View File

@ -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={
<div class={styles.randomTopicHeaderContainer}>
<div class={styles.randomTopicHeader}>{randomTopic().title}</div>
<div class={styles.randomTopicHeader}>{capitalize(randomTopic().title, true)}</div>
<div>
<a
class={styles.randomTopicHeaderLink}

View File

@ -1,3 +1,6 @@
.trigger {
white-space: nowrap;
}
.chevron {
vertical-align: top;

View File

@ -43,7 +43,7 @@ export const DropDown = <TOption extends Option = Option>(props: Props<TOption>)
<Show when={props.currentOption} keyed={true}>
<Popup
trigger={
<div class={props.triggerCssClass}>
<div class={clsx(styles.trigger, props.triggerCssClass)}>
{props.currentOption.title}{' '}
<Chevron
class={clsx(styles.chevron, {

View File

@ -2,12 +2,15 @@ import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { UserSearch } from '../UserSearch'
export const InviteCoAuthorsModal = () => {
type Props = {
title?: string
}
export const InviteCoAuthorsModal = (props: Props) => {
const { t } = useLocalize()
return (
<Modal variant="medium" name="inviteCoAuthors">
<h2>{t('Invite collaborators')}</h2>
<h2>{props.title || t('Invite collaborators')}</h2>
<UserSearch placeholder={t('Write your colleagues name or email')} onChange={() => {}} />
</Modal>
)

View File

@ -36,9 +36,7 @@ export const Popup = (props: PopupProps) => {
setIsVisible(false)
},
})
const toggle = () => setIsVisible((oldVisible) => !oldVisible)
return (
<span class={clsx(styles.container, props.containerCssClass)} ref={(el) => (containerRef.current = el)}>
<span class={styles.trigger} onClick={toggle}>

View File

@ -2,6 +2,7 @@
display: flex;
justify-content: flex-end;
position: relative;
min-width: 100px;
&.bordered {
border: 2px solid var(--black-100);

View File

@ -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;
}
}

View File

@ -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 (
<div class={clsx(styles.SoonChip, props.class)}>
<Icon name="lightning" class={styles.icon} />
{t('Soon')}
</div>
)
}

View File

@ -0,0 +1 @@
export { SoonChip } from './SoonChip'

View File

@ -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<typeof createBreakpoints>
}
const MediaQueryContext = createContext<MediaQueryContextType>()
export function useMediaQuery() {
return useContext(MediaQueryContext)
}
export const MediaQueryProvider = (props: { children: JSX.Element }) => {
const mediaMatches = createBreakpoints(breakpoints)
const value: MediaQueryContextType = { mediaMatches }
return <MediaQueryContext.Provider value={value}>{props.children}</MediaQueryContext.Provider>
}

View File

@ -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,
})

View File

@ -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() }),
])