Merge remote-tracking branch 'hub/main' into feature/sse-connect
Some checks failed
deploy / test (push) Failing after 1m7s
Some checks failed
deploy / test (push) Failing after 1m7s
This commit is contained in:
commit
3af0c47738
3
public/icons/lightning.svg
Normal file
3
public/icons/lightning.svg
Normal 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 |
|
@ -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",
|
||||
|
|
|
@ -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": "Спецпроекты",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
92
src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx
Normal file
92
src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx
Normal 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')} />
|
||||
</>
|
||||
)
|
||||
}
|
1
src/components/Feed/FeedArticlePopup/index.ts
Normal file
1
src/components/Feed/FeedArticlePopup/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { FeedArticlePopup } from './FeedArticlePopup'
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -75,4 +75,9 @@
|
|||
|
||||
.viewSwitcher {
|
||||
margin-bottom: 2rem;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 />}>
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 44px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.randomTopicHeaderLink {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
.trigger {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chevron {
|
||||
vertical-align: top;
|
||||
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
|
||||
&.bordered {
|
||||
border: 2px solid var(--black-100);
|
||||
|
|
23
src/components/_shared/SoonChip/SoonChip.module.scss
Normal file
23
src/components/_shared/SoonChip/SoonChip.module.scss
Normal 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;
|
||||
}
|
||||
}
|
20
src/components/_shared/SoonChip/SoonChip.tsx
Normal file
20
src/components/_shared/SoonChip/SoonChip.tsx
Normal 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>
|
||||
)
|
||||
}
|
1
src/components/_shared/SoonChip/index.ts
Normal file
1
src/components/_shared/SoonChip/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { SoonChip } from './SoonChip'
|
31
src/context/mediaQuery.tsx
Normal file
31
src/context/mediaQuery.tsx
Normal 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>
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
@ -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() }),
|
||||
])
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user