parent
12f5479957
commit
714c11e591
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)}*/}
|
||||||
|
|
|
@ -7,6 +7,7 @@ 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 { 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,9 @@ 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}
|
||||||
containerCssClass={stylesHeader.control}
|
containerCssClass={stylesHeader.control}
|
||||||
trigger={<Icon name="share-outline" class={styles.icon} />}
|
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,45 +1,59 @@
|
||||||
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 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(window.location.href)
|
||||||
|
}
|
||||||
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>
|
||||||
|
|
|
@ -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'
|
||||||
|
@ -11,6 +11,7 @@ import { CardTopic } from './CardTopic'
|
||||||
import { RatingControl } from '../Article/RatingControl'
|
import { RatingControl } from '../Article/RatingControl'
|
||||||
import { SharePopup } from '../Article/SharePopup'
|
import { 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?: {
|
||||||
|
@ -72,6 +73,12 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
const { cover, layout, slug, authors, stat } = props.article
|
const { cover, layout, slug, authors, stat } = props.article
|
||||||
|
|
||||||
|
const [url, setUrl] = createSignal<string>(null)
|
||||||
|
onMount(() => {
|
||||||
|
const composeUrl = new URL(location.href)
|
||||||
|
setUrl(composeUrl.origin)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
||||||
|
@ -193,6 +200,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={`${url()}/${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)} />
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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 { 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={location.href}
|
||||||
|
description={getDescription(props.articleBody)}
|
||||||
onVisibilityChange={(isVisible) => {
|
onVisibilityChange={(isVisible) => {
|
||||||
setIsSharePopupVisible(isVisible)
|
setIsSharePopupVisible(isVisible)
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
8
src/utils/meta.ts
Normal file
8
src/utils/meta.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const getDescription = (body: string) => {
|
||||||
|
if (!body) return null
|
||||||
|
const descriptionWordsArray = body
|
||||||
|
.slice(0, 150)
|
||||||
|
.replaceAll(/<[^>]*>/g, '')
|
||||||
|
.split(' ')
|
||||||
|
return descriptionWordsArray.splice(0, descriptionWordsArray.length - 1).join(' ') + '...'
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user