Article action menu (#352)

* Article action menu
This commit is contained in:
Ilya Y 2024-01-05 22:31:28 +03:00 committed by GitHub
parent 802ce84928
commit 0e6bb81b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 299 additions and 101 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 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": "Close",
"Co-author": "Co-author",
"Collaborate": "Help Edit",
"Collaborators": "Collaborators",
@ -217,6 +218,7 @@
"Last rev.": "Посл. изм.",
"Let's log in": "Let's log in",
"Link copied": "Link copied",
"Link copied to clipboard": "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",
@ -326,6 +328,7 @@
"Send link again": "Send link again",
"Settings": "Settings",
"Share": "Share",
"Share publication": "Share publication",
"Show": "Show",
"Show lyrics": "Show lyrics",
"Show more": "Show more",

View File

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

View File

@ -12,12 +12,15 @@ import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui'
import { getImageUrl } from '../../utils/getImageUrl'
import { getDescription, getKeywords } from '../../utils/meta'
import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image'
import { InviteCoAuthorsModal } from '../_shared/InviteCoAuthorsModal'
import { Lightbox } from '../_shared/Lightbox'
import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { AuthorBadge } from '../Author/AuthorBadge'
@ -58,6 +61,8 @@ const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const FullArticle = (props: Props) => {
const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate } = useLocalize()
const {
@ -66,8 +71,6 @@ export const FullArticle = (props: Props) => {
actions: { requireAuthentication },
} = useSession()
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
const mainTopic = createMemo(
@ -289,7 +292,7 @@ export const FullArticle = (props: Props) => {
const description = getDescription(props.article.description || body())
const ogTitle = props.article.title
const keywords = getKeywords(props.article)
const shareUrl = getShareUrl({ pathname: `/${props.article.slug}` })
return (
<>
<Meta name="descprition" content={description} />
@ -412,7 +415,7 @@ export const FullArticle = (props: Props) => {
<ShoutRatingControl shout={props.article} class={styles.ratingControl} />
</div>
<Popover content={t('Comment')}>
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => (
<div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}>
<Icon name="comment" class={styles.icon} />
@ -439,7 +442,7 @@ export const FullArticle = (props: Props) => {
</div>
</div>
<Popover content={t('Add to bookmarks')}>
<Popover content={t('Add to bookmarks')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => (
<div
class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)}
@ -454,14 +457,16 @@ export const FullArticle = (props: Props) => {
)}
</Popover>
<Popover content={t('Share')}>
<Popover content={t('Share')} disabled={isActionPopupActive()}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}>
<SharePopup
title={props.article.title}
description={description}
imageUrl={props.article.cover}
shareUrl={shareUrl}
containerCssClass={stylesHeader.control}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={
<div class={styles.shoutStatsItemInner}>
<Icon name="share-outline" class={styles.icon} />
@ -492,9 +497,9 @@ export const FullArticle = (props: Props) => {
<FeedArticlePopup
isOwner={canEdit()}
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
title={props.article.title}
description={description}
imageUrl={props.article.cover}
onShareClick={() => showModal('share')}
onInviteClick={() => showModal('inviteCoAuthors')}
onVisibilityChange={(isVisible) => setIsActionPopupActive(isVisible)}
trigger={
<button>
<Icon name="ellipsis" class={clsx(styles.icon)} />
@ -554,6 +559,13 @@ export const FullArticle = (props: Props) => {
<Show when={selectedImage()}>
<Lightbox image={selectedImage()} onClose={handleLightboxClose} />
</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 { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
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 styles from '../_shared/Popup/Popup.module.scss'
import { ShareLinks } from '../_shared/ShareLinks'
type SharePopupProps = {
title: string
shareUrl?: string
shareUrl: string
imageUrl: string
description: string
onVisibilityChange?: (value: boolean) => void
@ -25,63 +20,22 @@ export const getShareUrl = (params: { pathname?: string } = {}) => {
}
export const SharePopup = (props: SharePopupProps) => {
const { t } = useLocalize()
const [isVisible, setIsVisible] = createSignal(false)
const {
actions: { showSnackbar },
} = useSnackbar()
createEffect(() => {
if (props.onVisibilityChange) {
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 (
<Popup {...props} variant="bordered" onVisibilityChange={(value) => setIsVisible(value)}>
<ul class="nodash">
<li>
<button role="button" class={styles.shareControl} onClick={() => share(VK)}>
<Icon name="vk-white" class={styles.icon} />
VK
</button>
</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>
<ShareLinks
variant="inPopup"
title={props.title}
shareUrl={props.shareUrl}
imageUrl={props.imageUrl}
description={props.description}
/>
</Popup>
)
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
.backdrop {
align-items: center;
background: rgb(20 20 20 / 70%);
display: flex;
background: rgba(20, 20, 20, 0.07);
justify-content: center;
height: 100%;
left: 0;
@ -17,6 +17,7 @@
background: var(--background-color);
max-width: 1000px;
position: relative;
z-index: 1;
&:not([class*='col-']) {
width: 100%;

View File

@ -4,6 +4,7 @@ import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useMediaQuery } from '../../../context/mediaQuery'
import { router } from '../../../stores/router'
import { hideModal, useModalStore } from '../../../stores/ui'
@ -24,6 +25,7 @@ interface Props {
}
export const Modal = (props: Props) => {
const { t } = useLocalize()
const { modal } = useModalStore()
const [visible, setVisible] = createSignal(false)
const allowClose = createMemo(() => props.allowClose !== false)

View File

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

View File

@ -15,7 +15,7 @@
position: absolute;
text-align: left;
top: calc(100% + 8px);
z-index: 100;
z-index: 101;
ul {
margin-bottom: 0;
@ -103,23 +103,6 @@
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

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,104 @@
import { getPagePath } from '@nanostores/router'
import { createSocialShare, FACEBOOK, TELEGRAM, TWITTER, VK } from '@solid-primitives/share'
import { Input } from '@thisbeyond/solid-select'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSnackbar } from '../../../context/snackbar'
import { router } from '../../../stores/router'
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,30 @@
import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { Button } from '../Button'
import { ShareLinks } from '../ShareLinks'
import styles from '../ShareLinks/ShareLinks.module.scss'
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

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