parent
12f5479957
commit
714c11e591
|
@ -35,6 +35,8 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.216.0",
|
||||
"@aws-sdk/lib-storage": "^3.223.0",
|
||||
"@solid-primitives/share": "^2.0.1",
|
||||
"astro-seo-meta": "^2.0.0",
|
||||
"formidable": "^2.1.1",
|
||||
"mailgun.js": "^8.0.2"
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useSession } from '../../context/session'
|
|||
import { ReactionKind } from '../../graphql/types.gen'
|
||||
import CommentEditor from '../_shared/CommentEditor'
|
||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
|
||||
type Props = {
|
||||
comment: Reaction
|
||||
|
@ -152,15 +153,17 @@ const Comment = (props: Props) => {
|
|||
</button>
|
||||
</Show>
|
||||
|
||||
<SharePopup
|
||||
containerCssClass={stylesHeader.control}
|
||||
trigger={
|
||||
<button class={clsx(styles.commentControl, styles.commentControlShare)}>
|
||||
<Icon name="share" class={styles.icon} />
|
||||
{t('Share')}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
{/*<SharePopup*/}
|
||||
{/* title={'artile.title'}*/}
|
||||
{/* description={getDescription(body())}*/}
|
||||
{/* containerCssClass={stylesHeader.control}*/}
|
||||
{/* trigger={*/}
|
||||
{/* <button class={clsx(styles.commentControl, styles.commentControlShare)}>*/}
|
||||
{/* <Icon name="share" class={styles.icon} />*/}
|
||||
{/* {t('Share')}*/}
|
||||
{/* </button>*/}
|
||||
{/* }*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/*<button*/}
|
||||
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { Author, Shout } from '../../graphql/types.gen'
|
|||
import { t } from '../../utils/intl'
|
||||
import MD from './MD'
|
||||
import { SharePopup } from './SharePopup'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
import stylesHeader from '../Nav/Header.module.scss'
|
||||
import styles from '../../styles/Article.module.scss'
|
||||
import { RatingControl } from './RatingControl'
|
||||
|
@ -170,6 +171,9 @@ export const FullArticle = (props: ArticleProps) => {
|
|||
|
||||
<div class={styles.shoutStatsItem}>
|
||||
<SharePopup
|
||||
title={props.article.title}
|
||||
description={getDescription(props.article.body)}
|
||||
imageUrl={props.article.cover}
|
||||
containerCssClass={stylesHeader.control}
|
||||
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||
/>
|
||||
|
|
|
@ -1,45 +1,59 @@
|
|||
import { Icon } from '../_shared/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
|
||||
|
||||
import styles from '../_shared/Popup/Popup.module.scss'
|
||||
import type { PopupProps } 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) => {
|
||||
const [share] = createSocialShare(() => ({
|
||||
title: props.title,
|
||||
url: props.shareUrl,
|
||||
description: props.description
|
||||
}))
|
||||
const copyLink = async () => {
|
||||
await navigator.clipboard.writeText(window.location.href)
|
||||
}
|
||||
return (
|
||||
<Popup {...props} variant="bordered">
|
||||
<ul class="nodash">
|
||||
<li>
|
||||
<a href="#">
|
||||
<button role="button" onClick={() => share(VK)}>
|
||||
<Icon name="vk-white" class={styles.icon} />
|
||||
VK
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<button role="button" onClick={() => share(FACEBOOK)}>
|
||||
<Icon name="facebook-white" class={styles.icon} />
|
||||
Facebook
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<button role="button" onClick={() => share(TWITTER)}>
|
||||
<Icon name="twitter-white" class={styles.icon} />
|
||||
Twitter
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<button role="button" onClick={() => share(TELEGRAM)}>
|
||||
<Icon name="telegram-white" class={styles.icon} />
|
||||
Telegram
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#">
|
||||
<button role="button" onClick={copyLink}>
|
||||
<Icon name="link-white" class={styles.icon} />
|
||||
{t('Copy link')}
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popup>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { capitalize } from '../../utils'
|
||||
import { translit } from '../../utils/ru2en'
|
||||
|
@ -11,6 +11,7 @@ import { CardTopic } from './CardTopic'
|
|||
import { RatingControl } from '../Article/RatingControl'
|
||||
import { SharePopup } from '../Article/SharePopup'
|
||||
import stylesHeader from '../Nav/Header.module.scss'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
|
||||
interface ArticleCardProps {
|
||||
settings?: {
|
||||
|
@ -72,6 +73,12 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
|
||||
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 (
|
||||
<section
|
||||
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
|
||||
|
@ -193,6 +200,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
<div class={styles.shoutCardDetailsItem}>
|
||||
<SharePopup
|
||||
containerCssClass={stylesHeader.control}
|
||||
title={props.article['title']}
|
||||
description={getDescription(props.article['body'])}
|
||||
imageUrl={props.article['cover']}
|
||||
shareUrl={`${url()}/${slug}`}
|
||||
trigger={
|
||||
<button>
|
||||
<Icon name="share-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
|
||||
|
|
|
@ -21,7 +21,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
|||
const { topicEntities } = useTopicsStore()
|
||||
|
||||
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) => {
|
||||
|
@ -97,7 +97,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
|||
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
|
||||
>
|
||||
<small>@{authorSlug}</small>
|
||||
{authorEntities()[authorSlug].name}
|
||||
{authorEntities()[authorSlug]?.name}
|
||||
</a>
|
||||
</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 { clsx } from 'clsx'
|
||||
import styles from './Message.module.scss'
|
||||
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 { Icon } from '../_shared/Icon'
|
||||
import { MessageActionsPopup } from './MessageActionsPopup'
|
||||
import QuotedMessage from './QuotedMessage'
|
||||
|
||||
type Props = {
|
||||
content: Message
|
||||
content: MessageType
|
||||
ownId: number
|
||||
members: ChatMember[]
|
||||
replyClick?: () => void
|
||||
|
|
|
@ -10,6 +10,7 @@ import { getPagePath } from '@nanostores/router'
|
|||
import { clsx } from 'clsx'
|
||||
import { HeaderAuth } from './HeaderAuth'
|
||||
import { SharePopup } from '../Article/SharePopup'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
|
||||
const resources: { name: string; route: keyof Routes }[] = [
|
||||
{ name: t('zine'), route: 'home' },
|
||||
|
@ -20,6 +21,8 @@ const resources: { name: string; route: keyof Routes }[] = [
|
|||
type Props = {
|
||||
title?: string
|
||||
isHeaderFixed?: boolean
|
||||
articleBody?: string
|
||||
cover?: string
|
||||
}
|
||||
|
||||
export const Header = (props: Props) => {
|
||||
|
@ -123,6 +126,10 @@ export const Header = (props: Props) => {
|
|||
<Show when={props.title}>
|
||||
<div class={styles.articleControls}>
|
||||
<SharePopup
|
||||
title={props.title}
|
||||
imageUrl={props.cover}
|
||||
shareUrl={location.href}
|
||||
description={getDescription(props.articleBody)}
|
||||
onVisibilityChange={(isVisible) => {
|
||||
setIsSharePopupVisible(isVisible)
|
||||
}}
|
||||
|
|
|
@ -37,7 +37,7 @@ export const ArticlePage = (props: PageProps) => {
|
|||
})
|
||||
|
||||
return (
|
||||
<PageWrap headerTitle={article()?.title || ''}>
|
||||
<PageWrap headerTitle={article()?.title || ''} articleBody={article()?.body} cover={article()?.cover}>
|
||||
<Show when={Boolean(article())} fallback={<Loading />}>
|
||||
<ArticleView article={article()} />
|
||||
</Show>
|
||||
|
|
|
@ -8,6 +8,8 @@ import { clsx } from 'clsx'
|
|||
|
||||
type PageWrapProps = {
|
||||
headerTitle?: string
|
||||
articleBody?: string
|
||||
cover?: string
|
||||
children: JSX.Element
|
||||
isHeaderFixed?: boolean
|
||||
hideFooter?: boolean
|
||||
|
@ -19,7 +21,12 @@ export const PageWrap = (props: PageWrapProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} />
|
||||
<Header
|
||||
title={props.headerTitle}
|
||||
articleBody={props.articleBody}
|
||||
cover={props.articleBody}
|
||||
isHeaderFixed={isHeaderFixed}
|
||||
/>
|
||||
<main
|
||||
class={clsx('main-content', props.class)}
|
||||
classList={{ 'main-content--no-padding': !isHeaderFixed }}
|
||||
|
|
|
@ -2,20 +2,34 @@
|
|||
import { setLocale } from './stores/ui'
|
||||
import './styles/app.scss'
|
||||
import { t } from './utils/intl'
|
||||
import { Seo } from "astro-seo-meta"
|
||||
|
||||
// Setting locale for prerendered content here
|
||||
|
||||
const lang = Astro.url.searchParams.get('lang') || 'ru'
|
||||
console.log('[main.astro] astro runtime locale is', 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'}>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<title>{t('Discours')}</title>
|
||||
<Seo
|
||||
title={title}
|
||||
icon="favicon.png"
|
||||
facebook={{
|
||||
image: imageUrl,
|
||||
type: "website",
|
||||
}}
|
||||
twitter={{
|
||||
image: imageUrl,
|
||||
card: description,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
|
|
@ -3,21 +3,11 @@ import { Root } from '../components/Root'
|
|||
import Prerendered from '../main.astro'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
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()
|
||||
if (slug.endsWith('.map') || slug in excludes) {
|
||||
if (slug.includes('.')) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
|
@ -30,8 +20,16 @@ const { pathname, search } = Astro.url
|
|||
initRouter(pathname, search)
|
||||
|
||||
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 />
|
||||
</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