Merge branch 'dev_reserve' into 'dev'

Social share (#88)

See merge request discoursio/discoursio-webapp!8
This commit is contained in:
Igor 2023-01-31 14:02:34 +00:00
commit 4e28a92233
14 changed files with 2729 additions and 2604 deletions

View File

@ -35,6 +35,8 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.216.0", "@aws-sdk/client-s3": "^3.216.0",
"@aws-sdk/lib-storage": "^3.223.0", "@aws-sdk/lib-storage": "^3.223.0",
"@solid-primitives/share": "^2.0.1",
"astro-seo-meta": "^2.0.0",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"mailgun.js": "^8.0.2" "mailgun.js": "^8.0.2"
}, },

View File

@ -15,6 +15,7 @@ import { useSession } from '../../context/session'
import { ReactionKind } from '../../graphql/types.gen' import { ReactionKind } from '../../graphql/types.gen'
import CommentEditor from '../_shared/CommentEditor' import CommentEditor from '../_shared/CommentEditor'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { getDescription } from '../../utils/meta'
type Props = { type Props = {
comment: Reaction comment: Reaction
@ -152,15 +153,17 @@ const Comment = (props: Props) => {
</button> </button>
</Show> </Show>
<SharePopup {/*<SharePopup*/}
containerCssClass={stylesHeader.control} {/* title={'artile.title'}*/}
trigger={ {/* description={getDescription(body())}*/}
<button class={clsx(styles.commentControl, styles.commentControlShare)}> {/* containerCssClass={stylesHeader.control}*/}
<Icon name="share" class={styles.icon} /> {/* trigger={*/}
{t('Share')} {/* <button class={clsx(styles.commentControl, styles.commentControlShare)}>*/}
</button> {/* <Icon name="share" class={styles.icon} />*/}
} {/* {t('Share')}*/}
/> {/* </button>*/}
{/* }*/}
{/*/>*/}
{/*<button*/} {/*<button*/}
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/} {/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}

View File

@ -6,7 +6,8 @@ import { createMemo, For, Match, onMount, Show, Switch } from 'solid-js'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import MD from './MD' import MD from './MD'
import { SharePopup } from './SharePopup' import { getShareUrl, SharePopup } from './SharePopup'
import { getDescription } from '../../utils/meta'
import stylesHeader from '../Nav/Header.module.scss' import stylesHeader from '../Nav/Header.module.scss'
import styles from '../../styles/Article.module.scss' import styles from '../../styles/Article.module.scss'
import { RatingControl } from './RatingControl' import { RatingControl } from './RatingControl'
@ -170,6 +171,10 @@ export const FullArticle = (props: ArticleProps) => {
<div class={styles.shoutStatsItem}> <div class={styles.shoutStatsItem}>
<SharePopup <SharePopup
title={props.article.title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
shareUrl={getShareUrl()}
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
trigger={<Icon name="share-outline" class={styles.icon} />} trigger={<Icon name="share-outline" class={styles.icon} />}
/> />

View File

@ -1,45 +1,64 @@
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
import styles from '../_shared/Popup/Popup.module.scss' import styles from '../_shared/Popup/Popup.module.scss'
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
type SharePopupProps = Omit<PopupProps, 'children'> type SharePopupProps = {
title: string
shareUrl?: string
imageUrl: string
description: string
} & Omit<PopupProps, 'children'>
export const getShareUrl = (params: { pathname?: string } = {}) => {
if (typeof location === 'undefined') return ''
const pathname = params.pathname ?? location.pathname
return location.origin + pathname
}
export const SharePopup = (props: SharePopupProps) => { export const SharePopup = (props: SharePopupProps) => {
const [share] = createSocialShare(() => ({
title: props.title,
url: props.shareUrl,
description: props.description
}))
const copyLink = async () => {
await navigator.clipboard.writeText(props.shareUrl)
}
return ( return (
<Popup {...props} variant="bordered"> <Popup {...props} variant="bordered">
<ul class="nodash"> <ul class="nodash">
<li> <li>
<a href="#"> <button role="button" onClick={() => share(VK)}>
<Icon name="vk-white" class={styles.icon} /> <Icon name="vk-white" class={styles.icon} />
VK VK
</a> </button>
</li> </li>
<li> <li>
<a href="#"> <button role="button" onClick={() => share(FACEBOOK)}>
<Icon name="facebook-white" class={styles.icon} /> <Icon name="facebook-white" class={styles.icon} />
Facebook Facebook
</a> </button>
</li> </li>
<li> <li>
<a href="#"> <button role="button" onClick={() => share(TWITTER)}>
<Icon name="twitter-white" class={styles.icon} /> <Icon name="twitter-white" class={styles.icon} />
Twitter Twitter
</a> </button>
</li> </li>
<li> <li>
<a href="#"> <button role="button" onClick={() => share(TELEGRAM)}>
<Icon name="telegram-white" class={styles.icon} /> <Icon name="telegram-white" class={styles.icon} />
Telegram Telegram
</a> </button>
</li> </li>
<li> <li>
<a href="#"> <button role="button" onClick={copyLink}>
<Icon name="link-white" class={styles.icon} /> <Icon name="link-white" class={styles.icon} />
{t('Copy link')} {t('Copy link')}
</a> </button>
</li> </li>
</ul> </ul>
</Popup> </Popup>

View File

@ -1,5 +1,5 @@
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { createMemo, For, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { capitalize } from '../../utils' import { capitalize } from '../../utils'
import { translit } from '../../utils/ru2en' import { translit } from '../../utils/ru2en'
@ -9,8 +9,9 @@ import { locale } from '../../stores/ui'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { CardTopic } from './CardTopic' import { CardTopic } from './CardTopic'
import { RatingControl } from '../Article/RatingControl' import { RatingControl } from '../Article/RatingControl'
import { SharePopup } from '../Article/SharePopup' import { getShareUrl, SharePopup } from '../Article/SharePopup'
import stylesHeader from '../Nav/Header.module.scss' import stylesHeader from '../Nav/Header.module.scss'
import { getDescription } from '../../utils/meta'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -193,6 +194,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardDetailsItem}> <div class={styles.shoutCardDetailsItem}>
<SharePopup <SharePopup
containerCssClass={stylesHeader.control} containerCssClass={stylesHeader.control}
title={props.article['title']}
description={getDescription(props.article['body'])}
imageUrl={props.article['cover']}
shareUrl={getShareUrl({ pathname: `/${slug}` })}
trigger={ trigger={
<button> <button>
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />

View File

@ -21,7 +21,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
const { topicEntities } = useTopicsStore() const { topicEntities } = useTopicsStore()
const checkTopicIsSeen = (topicSlug: string) => { const checkTopicIsSeen = (topicSlug: string) => {
return articlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug])) return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug]))
} }
const checkAuthorIsSeen = (authorSlug: string) => { const checkAuthorIsSeen = (authorSlug: string) => {
@ -97,7 +97,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }} classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
> >
<small>@{authorSlug}</small> <small>@{authorSlug}</small>
{authorEntities()[authorSlug].name} {authorEntities()[authorSlug]?.name}
</a> </a>
</li> </li>
)} )}

View File

@ -1,16 +1,16 @@
import { createEffect, createMemo, createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './Message.module.scss' import styles from './Message.module.scss'
import DialogAvatar from './DialogAvatar' import DialogAvatar from './DialogAvatar'
import type { Message, ChatMember } from '../../graphql/types.gen' import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
import formattedTime from '../../utils/formatDateTime' import formattedTime from '../../utils/formatDateTime'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { MessageActionsPopup } from './MessageActionsPopup' import { MessageActionsPopup } from './MessageActionsPopup'
import QuotedMessage from './QuotedMessage' import QuotedMessage from './QuotedMessage'
type Props = { type Props = {
content: Message content: MessageType
ownId: number ownId: number
members: ChatMember[] members: ChatMember[]
replyClick?: () => void replyClick?: () => void

View File

@ -9,7 +9,8 @@ import styles from './Header.module.scss'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { HeaderAuth } from './HeaderAuth' import { HeaderAuth } from './HeaderAuth'
import { SharePopup } from '../Article/SharePopup' import { getShareUrl, SharePopup } from '../Article/SharePopup'
import { getDescription } from '../../utils/meta'
const resources: { name: string; route: keyof Routes }[] = [ const resources: { name: string; route: keyof Routes }[] = [
{ name: t('zine'), route: 'home' }, { name: t('zine'), route: 'home' },
@ -20,6 +21,8 @@ const resources: { name: string; route: keyof Routes }[] = [
type Props = { type Props = {
title?: string title?: string
isHeaderFixed?: boolean isHeaderFixed?: boolean
articleBody?: string
cover?: string
} }
export const Header = (props: Props) => { export const Header = (props: Props) => {
@ -123,6 +126,10 @@ export const Header = (props: Props) => {
<Show when={props.title}> <Show when={props.title}>
<div class={styles.articleControls}> <div class={styles.articleControls}>
<SharePopup <SharePopup
title={props.title}
imageUrl={props.cover}
shareUrl={getShareUrl()}
description={getDescription(props.articleBody)}
onVisibilityChange={(isVisible) => { onVisibilityChange={(isVisible) => {
setIsSharePopupVisible(isVisible) setIsSharePopupVisible(isVisible)
}} }}

View File

@ -37,7 +37,7 @@ export const ArticlePage = (props: PageProps) => {
}) })
return ( return (
<PageWrap headerTitle={article()?.title || ''}> <PageWrap headerTitle={article()?.title || ''} articleBody={article()?.body} cover={article()?.cover}>
<Show when={Boolean(article())} fallback={<Loading />}> <Show when={Boolean(article())} fallback={<Loading />}>
<ArticleView article={article()} /> <ArticleView article={article()} />
</Show> </Show>

View File

@ -8,6 +8,8 @@ import { clsx } from 'clsx'
type PageWrapProps = { type PageWrapProps = {
headerTitle?: string headerTitle?: string
articleBody?: string
cover?: string
children: JSX.Element children: JSX.Element
isHeaderFixed?: boolean isHeaderFixed?: boolean
hideFooter?: boolean hideFooter?: boolean
@ -19,7 +21,12 @@ export const PageWrap = (props: PageWrapProps) => {
return ( return (
<> <>
<Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} /> <Header
title={props.headerTitle}
articleBody={props.articleBody}
cover={props.articleBody}
isHeaderFixed={isHeaderFixed}
/>
<main <main
class={clsx('main-content', props.class)} class={clsx('main-content', props.class)}
classList={{ 'main-content--no-padding': !isHeaderFixed }} classList={{ 'main-content--no-padding': !isHeaderFixed }}

View File

@ -2,20 +2,34 @@
import { setLocale } from './stores/ui' import { setLocale } from './stores/ui'
import './styles/app.scss' import './styles/app.scss'
import { t } from './utils/intl' import { t } from './utils/intl'
import { Seo } from "astro-seo-meta"
// Setting locale for prerendered content here // Setting locale for prerendered content here
const lang = Astro.url.searchParams.get('lang') || 'ru' const lang = Astro.url.searchParams.get('lang') || 'ru'
console.log('[main.astro] astro runtime locale is', lang) console.log('[main.astro] astro runtime locale is', lang)
setLocale(lang) setLocale(lang)
const { protocol, host} = Astro.url
const title = Astro.props.title ?? t('Discours');
const imageUrl = Astro.props.imageUrl ?? `${protocol}${host}/public/bonfire.png`
const description = Astro.props.description ?? t('Horizontal collaborative journalistic platform')
--- ---
<html lang={lang || 'ru'}> <html lang={lang || 'ru'}>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/png" href="/favicon.png" /> <Seo
<title>{t('Discours')}</title> title={title}
icon="favicon.png"
facebook={{
image: imageUrl,
type: "website",
}}
twitter={{
image: imageUrl,
card: description,
}}
/>
</head> </head>
<body> <body>
<slot /> <slot />

View File

@ -3,21 +3,11 @@ import { Root } from '../components/Root'
import Prerendered from '../main.astro' import Prerendered from '../main.astro'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { initRouter } from '../stores/router' import { initRouter } from '../stores/router'
import {getDescription} from '../utils/meta'
const excludes = [
'authors',
'connect',
'create',
'inbox',
'search',
'topics',
'welcome',
'confirm',
'feed'
]
const slug = Astro.params.slug?.toString() const slug = Astro.params.slug?.toString()
if (slug.endsWith('.map') || slug in excludes) { if (slug.includes('.')) {
return Astro.redirect('/404') return Astro.redirect('/404')
} }
@ -30,8 +20,16 @@ const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate') Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
const title = article.title
const imageUrl = article.cover ?? ''
const description = article.body ? getDescription(article.body) : ''
--- ---
<Prerendered> <Prerendered
title={title}
imageUrl={imageUrl}
description={description}
>
<Root article={article} client:load /> <Root article={article} client:load />
</Prerendered> </Prerendered>

10
src/utils/meta.ts Normal file
View File

@ -0,0 +1,10 @@
export const getDescription = (body: string): string => {
if (!body) {
return ''
}
const descriptionWordsArray = body
.slice(0, 150) // meta description is roughly 155 characters long
.replaceAll(/<[^>]*>/g, '')
.split(' ')
return descriptionWordsArray.splice(0, descriptionWordsArray.length - 1).join(' ') + '...'
}

5167
yarn.lock

File diff suppressed because it is too large Load Diff