Merge branch 'feedback' of gitlab.com:discoursio/discoursio-webapp into feedback
This commit is contained in:
commit
b70e80dacd
|
@ -34,7 +34,12 @@
|
||||||
"vercel-build": "astro build"
|
"vercel-build": "astro build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.216.0",
|
||||||
|
"@aws-sdk/lib-storage": "^3.223.0",
|
||||||
"@connorskees/grass": "^0.12.0",
|
"@connorskees/grass": "^0.12.0",
|
||||||
|
"@solid-primitives/share": "^2.0.1",
|
||||||
|
"astro-seo-meta": "^2.0.0",
|
||||||
|
"formidable": "^2.1.1",
|
||||||
"mailgun.js": "^8.0.6"
|
"mailgun.js": "^8.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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 { 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'
|
||||||
|
@ -174,7 +174,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
title={props.article.title}
|
title={props.article.title}
|
||||||
description={getDescription(props.article.body)}
|
description={getDescription(props.article.body)}
|
||||||
imageUrl={props.article.cover}
|
imageUrl={props.article.cover}
|
||||||
shareUrl={location.href}
|
shareUrl={getShareUrl()}
|
||||||
containerCssClass={stylesHeader.control}
|
containerCssClass={stylesHeader.control}
|
||||||
trigger={<Icon name="share-outline" class={styles.icon} />}
|
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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 { 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'
|
||||||
|
@ -13,6 +12,12 @@ type SharePopupProps = {
|
||||||
description: string
|
description: string
|
||||||
} & Omit<PopupProps, 'children'>
|
} & 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(() => ({
|
const [share] = createSocialShare(() => ({
|
||||||
title: props.title,
|
title: props.title,
|
||||||
|
@ -20,7 +25,7 @@ export const SharePopup = (props: SharePopupProps) => {
|
||||||
description: props.description
|
description: props.description
|
||||||
}))
|
}))
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
await navigator.clipboard.writeText(window.location.href)
|
await navigator.clipboard.writeText(props.shareUrl)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Popup {...props} variant="bordered">
|
<Popup {...props} variant="bordered">
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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'
|
import { getDescription } from '../../utils/meta'
|
||||||
|
|
||||||
|
@ -73,12 +73,6 @@ 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 || ''}`)}
|
||||||
|
@ -203,7 +197,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
title={props.article['title']}
|
title={props.article['title']}
|
||||||
description={getDescription(props.article['body'])}
|
description={getDescription(props.article['body'])}
|
||||||
imageUrl={props.article['cover']}
|
imageUrl={props.article['cover']}
|
||||||
shareUrl={`${url()}/${slug}`}
|
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)} />
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
.sidebar {
|
||||||
|
max-height: calc(100vh - 120px);
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 1rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.counter {
|
.counter {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
|
@ -29,6 +37,7 @@
|
||||||
.settings {
|
.settings {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div class={styles.sidebar}>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="#">
|
<a href="#">
|
||||||
|
@ -78,6 +78,7 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
<Icon name="feed-notifications" class={styles.icon} />
|
<Icon name="feed-notifications" class={styles.icon} />
|
||||||
уведомления
|
уведомления
|
||||||
</a>
|
</a>
|
||||||
|
<span class={styles.counter}>283</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
@ -119,6 +120,6 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
{t('Feed settings')}
|
{t('Feed settings')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,10 @@
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: none;
|
background: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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'
|
import { getDescription } from '../../utils/meta'
|
||||||
|
|
||||||
const resources: { name: string; route: keyof Routes }[] = [
|
const resources: { name: string; route: keyof Routes }[] = [
|
||||||
|
@ -128,7 +128,7 @@ export const Header = (props: Props) => {
|
||||||
<SharePopup
|
<SharePopup
|
||||||
title={props.title}
|
title={props.title}
|
||||||
imageUrl={props.cover}
|
imageUrl={props.cover}
|
||||||
shareUrl={location.href}
|
shareUrl={getShareUrl()}
|
||||||
description={getDescription(props.articleBody)}
|
description={getDescription(props.articleBody)}
|
||||||
onVisibilityChange={(isVisible) => {
|
onVisibilityChange={(isVisible) => {
|
||||||
setIsSharePopupVisible(isVisible)
|
setIsSharePopupVisible(isVisible)
|
||||||
|
|
|
@ -14,16 +14,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
margin-bottom: 3rem;
|
margin: 0 0 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,99 +78,102 @@ export const FeedView = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="wide-container feed">
|
<div class="wide-container feed">
|
||||||
<div class="row">
|
<div class="shift-content">
|
||||||
<div class={clsx('col-md-3', styles.feedNavigation)}>
|
<div class={clsx('left-col', styles.feedNavigation)}>
|
||||||
<FeedSidebar authors={sortedAuthors()} />
|
<FeedSidebar authors={sortedAuthors()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="row">
|
||||||
<ul class="feed-filter">
|
<div class="col-md-8">
|
||||||
<Show when={!!session()?.user?.slug}>
|
<ul class="feed-filter">
|
||||||
<li class="selected">
|
<Show when={!!session()?.user?.slug}>
|
||||||
<a href="/feed/my">{t('My feed')}</a>
|
<li class="selected">
|
||||||
|
<a href="/feed/my">{t('My feed')}</a>
|
||||||
|
</li>
|
||||||
|
</Show>
|
||||||
|
<li>
|
||||||
|
<a href="/feed/?by=views">{t('Most read')}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/feed/?by=rating">{t('Top rated')}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/feed/?by=comments">{t('Most commented')}</a>
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
|
||||||
<li>
|
|
||||||
<a href="/feed/?by=views">{t('Most read')}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/feed/?by=rating">{t('Top rated')}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/feed/?by=comments">{t('Most commented')}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 0}>
|
|
||||||
<For each={sortedArticles().slice(0, 4)}>
|
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<div class={stylesBeside.besideColumnTitle}>
|
|
||||||
<h4>{t('Popular authors')}</h4>
|
|
||||||
<a href="/authors">
|
|
||||||
{t('All authors')}
|
|
||||||
<Icon name="arrow-right" class={stylesBeside.icon} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class={stylesBeside.besideColumn}>
|
|
||||||
<For each={topAuthors().slice(0, 5)}>
|
|
||||||
{(author) => (
|
|
||||||
<li>
|
|
||||||
<AuthorCard author={author} compact={true} hasLink={true} />
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<For each={sortedArticles().slice(4)}>
|
<Show when={sortedArticles().length > 0}>
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
<For each={sortedArticles().slice(0, 4)}>
|
||||||
</For>
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</Show>
|
</For>
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class={clsx('col-md-3', styles.feedAside)}>
|
<div class={stylesBeside.besideColumnTitle}>
|
||||||
<section class={styles.asideSection}>
|
<h4>{t('Popular authors')}</h4>
|
||||||
<h4>{t('Comments')}</h4>
|
<a href="/authors">
|
||||||
<For each={topComments()}>
|
{t('All authors')}
|
||||||
{/*FIXME: different components/better comment props*/}
|
<Icon name="arrow-right" class={stylesBeside.icon} />
|
||||||
{(comment) => <CommentCard comment={comment} reactions={[]} compact={true} />}
|
</a>
|
||||||
</For>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<Show when={topTopics().length > 0}>
|
<ul class={stylesBeside.besideColumn}>
|
||||||
|
<For each={topAuthors().slice(0, 5)}>
|
||||||
|
{(author) => (
|
||||||
|
<li>
|
||||||
|
<AuthorCard author={author} compact={true} hasLink={true} />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<For each={sortedArticles().slice(4)}>
|
||||||
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class={clsx('col-md-3', styles.feedAside)}>
|
||||||
<section class={styles.asideSection}>
|
<section class={styles.asideSection}>
|
||||||
<h4>{t('Topics')}</h4>
|
<h4>{t('Comments')}</h4>
|
||||||
<For each={topTopics().slice(0, 5)}>
|
<For each={topComments()}>
|
||||||
{(topic) => (
|
{/*FIXME: different components/better comment props*/}
|
||||||
<span class={clsx(stylesTopic.shoutTopic, styles.topic)}>
|
{(comment) => <CommentCard comment={comment} reactions={[]} compact={true} />}
|
||||||
<a href={`/topic/${topic.slug}`}>{topic.title}</a>{' '}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
</For>
|
||||||
</section>
|
</section>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<section class={clsx(styles.asideSection, styles.pinnedLinks)}>
|
<Show when={topTopics().length > 0}>
|
||||||
<Icon name="pin" class={styles.icon} />
|
<section class={styles.asideSection}>
|
||||||
<ul class="nodash">
|
<h4>{t('Topics')}</h4>
|
||||||
<li>
|
<For each={topTopics().slice(0, 5)}>
|
||||||
<a href="/about/guide">Как устроен Дискурс</a>
|
{(topic) => (
|
||||||
</li>
|
<span class={clsx(stylesTopic.shoutTopic, styles.topic)}>
|
||||||
<li>
|
<a href={`/topic/${topic.slug}`}>{topic.title}</a>{' '}
|
||||||
<a href="/how-to-write-a-good-article">Как создать хороший текст</a>
|
</span>
|
||||||
</li>
|
)}
|
||||||
<li>
|
</For>
|
||||||
<a href="#">Правила конструктивных дискуссий</a>
|
</section>
|
||||||
</li>
|
</Show>
|
||||||
<li>
|
|
||||||
<a href="/about/principles">Принципы сообщества</a>
|
<section class={clsx(styles.asideSection, styles.pinnedLinks)}>
|
||||||
</li>
|
<Icon name="pin" class={styles.icon} />
|
||||||
</ul>
|
<ul class="nodash">
|
||||||
</section>
|
<li>
|
||||||
</aside>
|
<a href="/about/guide">Как устроен Дискурс</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/how-to-write-a-good-article">Как создать хороший текст</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#">Правила конструктивных дискуссий</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/about/principles">Принципы сообщества</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={isLoadMoreButtonVisible()}>
|
<Show when={isLoadMoreButtonVisible()}>
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
<button class="button" onClick={loadMore}>
|
<button class="button" onClick={loadMore}>
|
||||||
|
|
|
@ -57,8 +57,8 @@
|
||||||
|
|
||||||
.feed-filter {
|
.feed-filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
@include font-size(1.7rem);
|
@include font-size(1.7rem);
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 0 1.6rem;
|
margin: 0 0 1.6rem;
|
||||||
|
@ -66,7 +66,7 @@
|
||||||
|
|
||||||
li {
|
li {
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 4px solid transparent;
|
||||||
margin: 0 1.4em 1em 0;
|
margin: 0 1.4em 0.5em 0;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
|
@ -691,11 +691,11 @@ astro-island {
|
||||||
.left-col {
|
.left-col {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
position: absolute;
|
|
||||||
right: 100%;
|
right: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(md) {
|
||||||
|
position: absolute;
|
||||||
width: 161px;
|
width: 161px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
export const getDescription = (body: string) => {
|
export const getDescription = (body: string): string => {
|
||||||
if (!body) return null
|
if (!body) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
const descriptionWordsArray = body
|
const descriptionWordsArray = body
|
||||||
.slice(0, 150)
|
.slice(0, 150) // meta description is roughly 155 characters long
|
||||||
.replaceAll(/<[^>]*>/g, '')
|
.replaceAll(/<[^>]*>/g, '')
|
||||||
.split(' ')
|
.split(' ')
|
||||||
return descriptionWordsArray.splice(0, descriptionWordsArray.length - 1).join(' ') + '...'
|
return descriptionWordsArray.splice(0, descriptionWordsArray.length - 1).join(' ') + '...'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user