Merge remote-tracking branch 'hub/main' into feature/sse-connect

This commit is contained in:
Untone 2024-01-08 10:11:40 +03:00
commit 45a5f0c542
27 changed files with 350 additions and 120 deletions

5
public/icons/copy.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="ic24_copy">
<path id="vector" fill-rule="evenodd" clip-rule="evenodd" d="M4 6C4 3.79086 5.79086 2 8 2H12C14.2091 2 16 3.79086 16 6V14C16 16.2091 14.2091 18 12 18H8C5.79086 18 4 16.2091 4 14V6ZM8 4C6.89543 4 6 4.89543 6 6V14C6 15.1046 6.89543 16 8 16H12C13.1046 16 14 15.1046 14 14V6C14 4.89543 13.1046 4 12 4H8ZM16.6344 6.90064C16.9109 6.42258 17.5227 6.25922 18.0007 6.53576C19.1937 7.22587 20 8.5182 20 10V18C20 20.2092 18.2091 22 16 22H12C10.5182 22 9.22586 21.1937 8.53575 20.0007C8.2592 19.5227 8.42257 18.911 8.90063 18.6344C9.37869 18.3579 9.99041 18.5212 10.267 18.9993C10.6143 19.5997 11.261 20 12 20H16C17.1046 20 18 19.1046 18 18V10C18 9.261 17.5997 8.61429 16.9993 8.26697C16.5212 7.99043 16.3579 7.3787 16.6344 6.90064Z" fill="#141414"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@ -72,6 +72,7 @@
"Choose a post type": "Choose a post type", "Choose a post type": "Choose a post type",
"Choose a title image for the article. You can immediately see how the publication card will look like.": "Choose a title image for the article. You can immediately see how the publication card will look like.", "Choose a title image for the article. You can immediately see how the publication card will look like.": "Choose a title image for the article. You can immediately see how the publication card will look like.",
"Choose who you want to write to": "Choose who you want to write to", "Choose who you want to write to": "Choose who you want to write to",
"Close": "Close",
"Co-author": "Co-author", "Co-author": "Co-author",
"Collaborate": "Help Edit", "Collaborate": "Help Edit",
"Collaborators": "Collaborators", "Collaborators": "Collaborators",
@ -217,6 +218,7 @@
"Last rev.": "Посл. изм.", "Last rev.": "Посл. изм.",
"Let's log in": "Let's log in", "Let's log in": "Let's log in",
"Link copied": "Link copied", "Link copied": "Link copied",
"Link copied to clipboard": "Link copied to clipboard",
"Link sent, check your email": "Link sent, check your email", "Link sent, check your email": "Link sent, check your email",
"List of authors of the open editorial community": "List of authors of the open editorial community", "List of authors of the open editorial community": "List of authors of the open editorial community",
"Lists": "Lists", "Lists": "Lists",
@ -320,6 +322,7 @@
"Send link again": "Send link again", "Send link again": "Send link again",
"Settings": "Settings", "Settings": "Settings",
"Share": "Share", "Share": "Share",
"Share publication": "Share publication",
"Show": "Show", "Show": "Show",
"Show lyrics": "Show lyrics", "Show lyrics": "Show lyrics",
"Show more": "Show more", "Show more": "Show more",
@ -356,6 +359,7 @@
"Suggest an idea": "Suggest an idea", "Suggest an idea": "Suggest an idea",
"Support Discours": "Support Discours", "Support Discours": "Support Discours",
"Support the project": "Support the project", "Support the project": "Support the project",
"Support us": "Support us",
"Terms of use": "Site rules", "Terms of use": "Site rules",
"Text checking": "Text checking", "Text checking": "Text checking",
"Thank you": "Thank you", "Thank you": "Thank you",

View File

@ -76,6 +76,7 @@
"Choose a post type": "Выберите тип публикации", "Choose a post type": "Выберите тип публикации",
"Choose a title image for the article. You can immediately see how the publication card will look like.": "Выберите заглавное изображение для статьи. Тут же сразу можно увидеть как будет выглядеть карточка публикации.", "Choose a title image for the article. You can immediately see how the publication card will look like.": "Выберите заглавное изображение для статьи. Тут же сразу можно увидеть как будет выглядеть карточка публикации.",
"Choose who you want to write to": "Выберите кому хотите написать", "Choose who you want to write to": "Выберите кому хотите написать",
"Close": "Закрыть",
"Co-author": "Соавтор", "Co-author": "Соавтор",
"Collaborate": "Помочь редактировать", "Collaborate": "Помочь редактировать",
"Collaborators": "Соавторы", "Collaborators": "Соавторы",
@ -229,6 +230,7 @@
"Last rev.": "Посл. изм.", "Last rev.": "Посл. изм.",
"Let's log in": "Давайте авторизуемся", "Let's log in": "Давайте авторизуемся",
"Link copied": "Ссылка скопирована", "Link copied": "Ссылка скопирована",
"Link copied to clipboard": "Ссылка скопирована в буфер обмена",
"Link sent, check your email": "Ссылка отправлена, проверьте почту", "Link sent, check your email": "Ссылка отправлена, проверьте почту",
"List of authors of the open editorial community": "Список авторов сообщества открытой редакции", "List of authors of the open editorial community": "Список авторов сообщества открытой редакции",
"Lists": "Списки", "Lists": "Списки",
@ -340,6 +342,7 @@
"Send link again": "Прислать ссылку ещё раз", "Send link again": "Прислать ссылку ещё раз",
"Settings": "Настройки", "Settings": "Настройки",
"Share": "Поделиться", "Share": "Поделиться",
"Share publication": "Поделиться публикацией",
"Short opening": "Расскажите вашу историю...", "Short opening": "Расскажите вашу историю...",
"Show": "Показать", "Show": "Показать",
"Show lyrics": "Текст песни", "Show lyrics": "Текст песни",
@ -378,6 +381,7 @@
"Suggest an idea": "Предложить идею", "Suggest an idea": "Предложить идею",
"Support Discours": "Поддержите Дискурс", "Support Discours": "Поддержите Дискурс",
"Support the project": "Поддержать проект", "Support the project": "Поддержать проект",
"Support us": "Помочь журналу",
"Terms of use": "Правила сайта", "Terms of use": "Правила сайта",
"Text checking": "Проверка текста", "Text checking": "Проверка текста",
"Thank you": "Благодарности", "Thank you": "Благодарности",

View File

@ -14,12 +14,15 @@ import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
import { isCyrillic } from '../../utils/cyrillic' import { isCyrillic } from '../../utils/cyrillic'
import { getImageUrl } from '../../utils/getImageUrl' import { showModal } from '../../stores/ui'
import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl'
import { getDescription, getKeywords } from '../../utils/meta' import { getDescription, getKeywords } from '../../utils/meta'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image' import { Image } from '../_shared/Image'
import { InviteCoAuthorsModal } from '../_shared/InviteCoAuthorsModal'
import { Lightbox } from '../_shared/Lightbox' import { Lightbox } from '../_shared/Lightbox'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
@ -60,6 +63,8 @@ const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const [selectedImage, setSelectedImage] = createSignal('') const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { const {
@ -68,8 +73,6 @@ export const FullArticle = (props: Props) => {
actions: { requireAuthentication }, actions: { requireAuthentication },
} = useSession() } = useSession()
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const formattedDate = createMemo(() => formatDate(new Date(props.article.created_at * 1000))) const formattedDate = createMemo(() => formatDate(new Date(props.article.created_at * 1000)))
const mainTopic = createMemo(() => { const mainTopic = createMemo(() => {
@ -292,12 +295,19 @@ export const FullArticle = (props: Props) => {
} }
} }
const ogImage = props.article.cover const cover = props.article.cover ?? 'production/image/logo_image.png'
? getImageUrl(props.article.cover, { width: 1200 })
: getImageUrl('production/image/logo_image.png') const ogImage = getOpenGraphImageUrl(cover, {
title: props.article.title,
topic: mainTopic().title,
author: props.article.authors[0].name,
width: 1200,
})
const description = getDescription(props.article.description || body()) const description = getDescription(props.article.description || body())
const ogTitle = props.article.title const ogTitle = props.article.title
const keywords = getKeywords(props.article) const keywords = getKeywords(props.article)
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
const getAuthorName = (a: Author) => { const getAuthorName = (a: Author) => {
return lang() === 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name return lang() === 'en' && isCyrillic(a.name) ? capitalize(a.slug.replace(/-/, ' ')) : a.name
} }
@ -426,7 +436,7 @@ export const FullArticle = (props: Props) => {
<ShoutRatingControl shout={props.article} class={styles.ratingControl} /> <ShoutRatingControl shout={props.article} class={styles.ratingControl} />
</div> </div>
<Popover content={t('Comment')}> <Popover content={t('Comment')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}> <div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}>
<Icon name="comment" class={styles.icon} /> <Icon name="comment" class={styles.icon} />
@ -453,7 +463,7 @@ export const FullArticle = (props: Props) => {
</div> </div>
</div> </div>
<Popover content={t('Add to bookmarks')}> <Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div <div
class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)} class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)}
@ -468,14 +478,16 @@ export const FullArticle = (props: Props) => {
)} )}
</Popover> </Popover>
<Popover content={t('Share')}> <Popover content={t('Share')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}> <div class={styles.shoutStatsItem} ref={triggerRef}>
<SharePopup <SharePopup
title={props.article.title} title={props.article.title}
description={description} description={description}
imageUrl={props.article.cover} imageUrl={props.article.cover}
shareUrl={shareUrl}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
<div class={styles.shoutStatsItemInner}> <div class={styles.shoutStatsItemInner}>
<Icon name="share-outline" class={styles.icon} /> <Icon name="share-outline" class={styles.icon} />
@ -506,9 +518,9 @@ export const FullArticle = (props: Props) => {
<FeedArticlePopup <FeedArticlePopup
isOwner={canEdit()} isOwner={canEdit()}
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)} containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
title={props.article.title} onShareClick={() => showModal('share')}
description={description} onInviteClick={() => showModal('inviteCoAuthors')}
imageUrl={props.article.cover} onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
<button> <button>
<Icon name="ellipsis" class={clsx(styles.icon)} /> <Icon name="ellipsis" class={clsx(styles.icon)} />
@ -570,6 +582,13 @@ export const FullArticle = (props: Props) => {
<Show when={selectedImage()}> <Show when={selectedImage()}>
<Lightbox image={selectedImage()} onClose={handleLightboxClose} /> <Lightbox image={selectedImage()} onClose={handleLightboxClose} />
</Show> </Show>
<InviteCoAuthorsModal title={t('Invite experts')} />
<ShareModal
title={props.article.title}
description={description}
imageUrl={props.article.cover}
shareUrl={shareUrl}
/>
</> </>
) )
} }

View File

@ -1,18 +1,13 @@
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
import { createEffect, createSignal } from 'solid-js' import { createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { ShareLinks } from '../_shared/ShareLinks'
import styles from '../_shared/Popup/Popup.module.scss'
type SharePopupProps = { type SharePopupProps = {
title: string title: string
shareUrl?: string shareUrl: string
imageUrl: string imageUrl: string
description: string description: string
onVisibilityChange?: (value: boolean) => void onVisibilityChange?: (value: boolean) => void
@ -25,63 +20,22 @@ export const getShareUrl = (params: { pathname?: string } = {}) => {
} }
export const SharePopup = (props: SharePopupProps) => { export const SharePopup = (props: SharePopupProps) => {
const { t } = useLocalize()
const [isVisible, setIsVisible] = createSignal(false) const [isVisible, setIsVisible] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
createEffect(() => { createEffect(() => {
if (props.onVisibilityChange) { if (props.onVisibilityChange) {
props.onVisibilityChange(isVisible()) props.onVisibilityChange(isVisible())
} }
}) })
const [share] = createSocialShare(() => ({
title: props.title,
url: props.shareUrl,
description: props.description,
}))
const copyLink = async () => {
await navigator.clipboard.writeText(props.shareUrl)
showSnackbar({ body: t('Link copied') })
}
return ( return (
<Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}> <Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}>
<ul class="nodash"> <ShareLinks
<li> variant="inPopup"
<button role="button" class={styles.shareControl} onClick={() => share(VK)}> title={props.title}
<Icon name="vk-white" class={styles.icon} /> shareUrl={props.shareUrl}
VK imageUrl={props.imageUrl}
</button> description={props.description}
</li> />
<li>
<button role="button" class={styles.shareControl} onClick={() => share(FACEBOOK)}>
<Icon name="facebook-white" class={styles.icon} />
Facebook
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => share(TWITTER)}>
<Icon name="twitter-white" class={styles.icon} />
Twitter
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => share(TELEGRAM)}>
<Icon name="telegram-white" class={styles.icon} />
Telegram
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={copyLink}>
<Icon name="link-white" class={styles.icon} />
{t('Copy link')}
</button>
</li>
</ul>
</Popup> </Popup>
) )
} }

View File

@ -107,7 +107,7 @@
height: 0; height: 0;
margin-bottom: 1.6rem; margin-bottom: 1.6rem;
overflow: hidden; overflow: hidden;
padding-bottom: 56.2%; //16:9 padding-bottom: 56.2%; // 16:9
position: relative; position: relative;
transform-origin: 50% 50%; transform-origin: 50% 50%;
transition: transform 1s ease-in-out; transition: transform 1s ease-in-out;

View File

@ -7,11 +7,14 @@ import { createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { showModal } from '../../../stores/ui'
import { capitalize } from '../../../utils/capitalize' import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { InviteCoAuthorsModal } from '../../_shared/InviteCoAuthorsModal'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import { ShareModal } from '../../_shared/ShareModal'
import { CoverImage } from '../../Article/CoverImage' import { CoverImage } from '../../Article/CoverImage'
import { getShareUrl, SharePopup } from '../../Article/SharePopup' import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl' import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
@ -310,7 +313,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardDetailsContent}> <div class={styles.shoutCardDetailsContent}>
<Show when={canEdit()}> <Show when={canEdit()}>
<Popover content={t('Edit')}> <Popover content={t('Edit')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}> <div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<a href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}> <a href={getPagePath(router, 'edit', { shoutId: props.article.id.toString() })}>
@ -325,7 +328,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Popover> </Popover>
</Show> </Show>
<Popover content={t('Add to bookmarks')}> <Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}> <div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<button> <button>
@ -348,6 +351,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
description={description} description={description}
imageUrl={props.article.cover} imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })} shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
<button> <button>
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
@ -366,9 +370,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
<FeedArticlePopup <FeedArticlePopup
isOwner={canEdit()} isOwner={canEdit()}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
title={title} onShareClick={() => showModal('share')}
description={description} onInviteClick={() => showModal('inviteCoAuthors')}
imageUrl={props.article.cover} onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={ trigger={
<button> <button>
<Icon name="ellipsis" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="ellipsis" class={clsx(styles.icon, styles.feedControlIcon)} />
@ -384,6 +388,13 @@ export const ArticleCard = (props: ArticleCardProps) => {
</section> </section>
</Show> </Show>
</div> </div>
<InviteCoAuthorsModal title={t('Invite experts')} />
<ShareModal
title={title}
description={description}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
/>
</section> </section>
) )
} }

View File

@ -5,6 +5,7 @@
padding: 0 !important; padding: 0 !important;
text-align: left; text-align: left;
overflow: hidden; overflow: hidden;
margin-top: -14px;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
left: auto !important; left: auto !important;
@ -30,6 +31,10 @@
&.soon { &.soon {
color: var(--black-300); color: var(--black-300);
display: flex;
gap: 0.6rem;
width: 100%;
justify-content: space-between;
} }
&:hover { &:hover {
@ -41,6 +46,7 @@
li:first-child .action { li:first-child .action {
padding-top: 16px; padding-top: 16px;
} }
li:last-child .action { li:last-child .action {
padding-bottom: 16px; padding-bottom: 16px;
} }

View File

@ -1,29 +1,31 @@
import type { PopupProps } from '../../_shared/Popup' import type { PopupProps } from '../../_shared/Popup'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createSignal, Show } from 'solid-js' import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { showModal } from '../../../stores/ui'
import { InviteCoAuthorsModal } from '../../_shared/InviteCoAuthorsModal'
import { Popup } from '../../_shared/Popup' import { Popup } from '../../_shared/Popup'
import { SoonChip } from '../../_shared/SoonChip' import { SoonChip } from '../../_shared/SoonChip'
import styles from './FeedArticlePopup.module.scss' import styles from './FeedArticlePopup.module.scss'
type FeedArticlePopupProps = { type FeedArticlePopupProps = {
title: string
imageUrl: string
isOwner: boolean isOwner: boolean
description: string onInviteClick: () => void
onShareClick: () => void
} & Omit<PopupProps, 'children'> } & Omit<PopupProps, 'children'>
export const FeedArticlePopup = (props: FeedArticlePopupProps) => { export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<> <>
<Popup {...props} variant="tiny" popupCssClass={styles.feedArticlePopup}> <Popup {...props} horizontalAnchor={'right'} variant="tiny" popupCssClass={styles.feedArticlePopup}>
<ul class={clsx('nodash', styles.actionList)}> <ul class={clsx('nodash', styles.actionList)}>
<li>
<button class={styles.action} role="button" onClick={props.onShareClick}>
{t('Share')}
</button>
</li>
<Show when={!props.isOwner}> <Show when={!props.isOwner}>
<li> <li>
<button <button
@ -38,13 +40,7 @@ export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
</li> </li>
</Show> </Show>
<li> <li>
<button <button class={styles.action} role="button" onClick={props.onInviteClick}>
class={styles.action}
role="button"
onClick={() => {
showModal('inviteCoAuthors')
}}
>
{t('Invite experts')} {t('Invite experts')}
</button> </button>
</li> </li>
@ -86,7 +82,6 @@ export const FeedArticlePopup = (props: FeedArticlePopupProps) => {
{/*</li>*/} {/*</li>*/}
</ul> </ul>
</Popup> </Popup>
<InviteCoAuthorsModal title={t('Invite experts')} />
</> </>
) )
} }

View File

@ -2,7 +2,6 @@ import { clsx } from 'clsx'
import { createEffect, createSignal, on, Show } from 'solid-js' import { createEffect, createSignal, on, Show } from 'solid-js'
import { useLocalize } from '../../../../context/localize' import { useLocalize } from '../../../../context/localize'
// import { resetSortedArticles } from '../../../../stores/zine/articles'
import { Icon } from '../../../_shared/Icon' import { Icon } from '../../../_shared/Icon'
import styles from './PasswordField.module.scss' import styles from './PasswordField.module.scss'

View File

@ -1,7 +1,7 @@
.backdrop { .backdrop {
align-items: center; align-items: center;
background: rgb(20 20 20 / 70%);
display: flex; display: flex;
background: rgb(20 20 20 / 7%);
justify-content: center; justify-content: center;
height: 100%; height: 100%;
left: 0; left: 0;
@ -17,6 +17,7 @@
background: var(--background-color); background: var(--background-color);
max-width: 1000px; max-width: 1000px;
position: relative; position: relative;
z-index: 1;
&:not([class*='col-']) { &:not([class*='col-']) {
width: 100%; width: 100%;
@ -124,11 +125,13 @@
.maxHeight { .maxHeight {
height: 100%; height: 100%;
} }
.container { .container {
padding: 0; padding: 0;
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
} }
.modalInner { .modalInner {
padding: 1rem 1rem 0; padding: 1rem 1rem 0;
height: 100%; height: 100%;

View File

@ -1,6 +1,7 @@
.TableOfContentsFixedWrapper { .TableOfContentsFixedWrapper {
min-height: 100%; min-height: 100%;
position: relative; position: relative;
z-index: 1;
top: 0; top: 0;
@include media-breakpoint-down(xl) { @include media-breakpoint-down(xl) {

View File

@ -7,9 +7,8 @@
.basicInfo { .basicInfo {
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap;
flex: 1; flex: 1;
gap: 1rem; gap: 1rem;
} }

View File

@ -4,10 +4,6 @@
padding: 0 0 4rem; padding: 0 0 4rem;
min-height: 100vh; min-height: 100vh;
.navigation {
padding: 0;
}
.showMore { .showMore {
display: flex; display: flex;
width: 100%; width: 100%;

View File

@ -147,7 +147,7 @@ export const Expo = (props: Props) => {
<div class={styles.Expo}> <div class={styles.Expo}>
<Show when={sortedArticles().length > 0} fallback={<Loading />}> <Show when={sortedArticles().length > 0} fallback={<Loading />}>
<div class="wide-container"> <div class="wide-container">
<ul class={clsx('view-switcher', styles.navigation)}> <ul class={clsx('view-switcher')}>
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}> <li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
<ConditionalWrapper <ConditionalWrapper
condition={Boolean(props.layout)} condition={Boolean(props.layout)}

View File

@ -195,6 +195,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 4rem; margin-bottom: 4rem;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
flex-direction: column-reverse; flex-direction: column-reverse;
align-items: flex-start; align-items: flex-start;
@ -205,6 +206,7 @@
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
min-width: 300px; min-width: 300px;
overflow: hidden;
& > li { & > li {
margin-bottom: 0; margin-bottom: 0;
@ -213,8 +215,7 @@
.dropdowns { .dropdowns {
display: flex; display: flex;
flex-direction: row; flex-flow: row nowrap;
flex-wrap: nowrap;
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
} }

View File

@ -269,6 +269,7 @@ export const FeedView = (props: Props) => {
<div class={styles.dropdowns}> <div class={styles.dropdowns}>
<Show when={searchParams().by && searchParams().by !== 'publish_date'}> <Show when={searchParams().by && searchParams().by !== 'publish_date'}>
<DropDown <DropDown
popupProps={{ horizontalAnchor: 'right' }}
options={periods} options={periods}
currentOption={currentPeriod()} currentOption={currentPeriod()}
triggerCssClass={styles.periodSwitcher} triggerCssClass={styles.periodSwitcher}
@ -276,6 +277,7 @@ export const FeedView = (props: Props) => {
/> />
</Show> </Show>
<DropDown <DropDown
popupProps={{ horizontalAnchor: 'right' }}
options={visibilities} options={visibilities}
currentOption={currentVisibility()} currentOption={currentVisibility()}
triggerCssClass={styles.periodSwitcher} triggerCssClass={styles.periodSwitcher}

View File

@ -1,6 +1,7 @@
.trigger { .trigger {
white-space: nowrap; white-space: nowrap;
} }
.chevron { .chevron {
vertical-align: top; vertical-align: top;
@ -8,3 +9,7 @@
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
.active {
font-weight: 600;
}

View File

@ -14,7 +14,7 @@ export type Option = {
type Props<TOption> = { type Props<TOption> = {
class?: string class?: string
popupProps?: PopupProps popupProps?: Partial<PopupProps>
options: TOption[] options: TOption[]
currentOption: TOption currentOption: TOption
triggerCssClass?: string triggerCssClass?: string
@ -56,9 +56,12 @@ export const DropDown = <TOption extends Option = Option>(props: Props<TOption>)
onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)} onVisibilityChange={(isVisible) => setIsPopupVisible(isVisible)}
{...props.popupProps} {...props.popupProps}
> >
<For each={props.options.filter((p) => p.value !== props.currentOption.value)}> <For each={props.options}>
{(option) => ( {(option) => (
<div class="link" onClick={() => props.onChange(option)}> <div
class={clsx('link', { [styles.active]: props.currentOption.value === option.value })}
onClick={() => props.onChange(option)}
>
{option.title} {option.title}
</div> </div>
)} )}

View File

@ -15,7 +15,7 @@
position: absolute; position: absolute;
text-align: left; text-align: left;
top: calc(100% + 8px); top: calc(100% + 8px);
z-index: 100; z-index: 101;
ul { ul {
margin-bottom: 0; margin-bottom: 0;
@ -103,23 +103,6 @@
vertical-align: middle; vertical-align: middle;
} }
} }
.shareControl {
text-align: left;
transition:
color 0.3s,
background-color 0.3s;
white-space: nowrap;
&:hover {
background: #000;
color: #fff;
.icon img {
filter: invert(0);
}
}
}
} }
// TODO: animation // TODO: animation

View File

@ -0,0 +1,82 @@
.ShareLinks {
.shareControl {
text-align: left;
transition:
color 0.3s,
background-color 0.3s;
white-space: nowrap;
&:hover {
background: #000;
color: #fff;
.icon img {
filter: invert(0);
}
}
.icon {
display: inline-block;
width: 3.6rem;
img {
display: inline-block;
filter: invert(1);
max-height: 2rem;
max-width: 2rem;
transition: filter 0.3s;
vertical-align: middle;
}
}
}
&.inModal {
li {
margin: 0;
}
.shareControl {
font-size: 18px;
font-weight: 600;
padding: 1rem;
margin: 0 -12px;
width: calc(100% + 24px);
transition: unset;
}
}
.linkInput {
position: relative;
margin-top: 2rem;
margin-bottom: -2rem !important;
input {
margin-bottom: 0;
padding-right: 40px;
box-sizing: border-box;
}
.copyButton {
position: absolute;
top: 2px;
bottom: 2px;
right: 2px;
width: 40px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
}
.isCopied {
@include font-size(1.6rem);
position: absolute;
top: 100%;
left: 1.2rem;
margin-top: 0.5rem;
color: var(--blue-500);
}
}

View File

@ -0,0 +1,101 @@
import { createSocialShare, FACEBOOK, TELEGRAM, TWITTER, VK } from '@solid-primitives/share'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSnackbar } from '../../../context/snackbar'
import { Icon } from '../Icon'
import { Popover } from '../Popover'
import styles from './ShareLinks.module.scss'
type Props = {
title: string
description: string
shareUrl: string
imageUrl?: string
class?: string
variant: 'inModal' | 'inPopup'
}
export const ShareLinks = (props: Props) => {
const { t } = useLocalize()
const [isLinkCopied, setIsLinkCopied] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
const [share] = createSocialShare(() => ({
title: props.title,
url: props.shareUrl,
description: props.description,
}))
const copyLink = async () => {
await navigator.clipboard.writeText(props.shareUrl)
if (props.variant === 'inModal') {
setIsLinkCopied(true)
setTimeout(() => setIsLinkCopied(false), 3000)
} else {
showSnackbar({ body: t('Link copied') })
}
}
return (
<div class={clsx(styles.ShareLinks, props.class, { [styles.inModal]: props.variant === 'inModal' })}>
<ul class="nodash">
<li>
<button role="button" class={styles.shareControl} onClick={() => share(FACEBOOK)}>
<Icon name="facebook-white" class={styles.icon} />
Facebook
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => share(TWITTER)}>
<Icon name="twitter-white" class={styles.icon} />
Twitter
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => share(TELEGRAM)}>
<Icon name="telegram-white" class={styles.icon} />
Telegram
</button>
</li>
<li>
<button role="button" class={styles.shareControl} onClick={() => share(VK)}>
<Icon name="vk-white" class={styles.icon} />
VK
</button>
</li>
<li>
<Show
when={props.variant === 'inModal'}
fallback={
<button role="button" class={styles.shareControl} onClick={copyLink}>
<Icon name="link-white" class={styles.icon} />
{t('Copy link')}
</button>
}
>
<form class={clsx('pretty-form__item', styles.linkInput)}>
<input type="text" name="link" readonly value={props.shareUrl} />
<label for="link">{t('Copy link')}</label>
<Popover content={t('Copy link')}>
{(triggerRef: (el) => void) => (
<div class={styles.copyButton} onClick={copyLink} ref={triggerRef}>
<Icon name="copy" class={styles.icon} />
</div>
)}
</Popover>
<Show when={isLinkCopied()}>
<div class={styles.isCopied}>{t('Link copied to clipboard')}</div>
</Show>
</form>
</Show>
</li>
</ul>
</div>
)
}

View File

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

View File

@ -0,0 +1,27 @@
import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { ShareLinks } from '../ShareLinks'
type Props = {
modalTitle?: string
shareUrl?: string
title: string
imageUrl: string
description: string
}
export const ShareModal = (props: Props) => {
const { t } = useLocalize()
return (
<Modal name="share" variant="medium" allowClose={true}>
<h2>{t('Share publication')}</h2>
<ShareLinks
variant="inModal"
title={props.title}
shareUrl={props.shareUrl}
imageUrl={props.imageUrl}
description={props.description}
/>
</Modal>
)
}

View File

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

View File

@ -25,6 +25,7 @@ export type ModalType =
| 'following' | 'following'
| 'search' | 'search'
| 'inviteCoAuthors' | 'inviteCoAuthors'
| 'share'
export const MODALS: Record<ModalType, ModalType> = { export const MODALS: Record<ModalType, ModalType> = {
auth: 'auth', auth: 'auth',
@ -42,6 +43,7 @@ export const MODALS: Record<ModalType, ModalType> = {
following: 'following', following: 'following',
inviteCoAuthors: 'inviteCoAuthors', inviteCoAuthors: 'inviteCoAuthors',
search: 'search', search: 'search',
share: 'share',
} }
const [modal, setModal] = createSignal<ModalType>(null) const [modal, setModal] = createSignal<ModalType>(null)

View File

@ -1,5 +1,7 @@
import { thumborUrl } from './config' import { thumborUrl } from './config'
const thumborPrefix = `${thumborUrl}/unsafe/`
const getSizeUrlPart = (options: { width?: number; height?: number } = {}) => { const getSizeUrlPart = (options: { width?: number; height?: number } = {}) => {
const widthString = options.width ? options.width.toString() : '' const widthString = options.width ? options.width.toString() : ''
const heightString = options.height ? options.height.toString() : '' const heightString = options.height ? options.height.toString() : ''
@ -19,3 +21,27 @@ export const getImageUrl = (src: string, options: { width?: number; height?: num
`${thumborUrl}/unsafe/${sizeUrlPart}${sourceUrl}`.replace('https://', '').replace('//', '/') `${thumborUrl}/unsafe/${sizeUrlPart}${sourceUrl}`.replace('https://', '').replace('//', '/')
) )
} }
export const getOpenGraphImageUrl = (
src: string,
options: {
topic: string
title: string
author: string
width?: number
height?: number
},
) => {
const sizeUrlPart = getSizeUrlPart(options)
const filtersPart = `filters:discourstext('${encodeURIComponent(options.topic)}','${encodeURIComponent(
options.author,
)}','${encodeURIComponent(options.title)}')/`
if (src.startsWith(thumborPrefix)) {
const thumborKey = src.replace(thumborPrefix, '')
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${thumborKey}`
}
return `${thumborUrl}/unsafe/${sizeUrlPart}${filtersPart}${src}`
}