Merge remote-tracking branch 'hub/fix/topic-header' into hotfix/following

This commit is contained in:
Untone 2024-05-30 21:58:52 +03:00
commit e6a4db2eb5
26 changed files with 780 additions and 291 deletions

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">
<path d="M19.125 12.75H4.5C4.08854 12.75 3.75 12.4115 3.75 12C3.75 11.5885 4.08854 11.25 4.5 11.25H19.125C19.5365 11.25 19.875 11.5885 19.875 12C19.875 12.4115 19.5365 12.75 19.125 12.75Z" fill="currentColor"/>
<path
d="M14.0678 18.3593C13.8803 18.3593 13.6928 18.2916 13.547 18.151C13.2501 17.8593 13.2397 17.3853 13.5314 17.0885L18.4584 11.9999L13.5314 6.91137C13.2397 6.6145 13.2501 6.14054 13.547 5.84887C13.8439 5.56241 14.3178 5.57283 14.6043 5.8697L20.0366 11.4791C20.3178 11.7707 20.3178 12.2291 20.0366 12.5207L14.6043 18.1301C14.4584 18.2864 14.2657 18.3593 14.0678 18.3593Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@ -55,6 +55,7 @@
"Be the first to rate": "Be the first to rate",
"Become an author": "Become an author",
"bold": "bold",
"Block rules": "За что можно получить бан",
"Bold": "Bold",
"Bookmarked": "Saved",
"bookmarks": "bookmarks",
@ -98,6 +99,7 @@
"Commenting": "Commenting",
"Comments": "Comments",
"CommentsWithCount": "{count, plural, =0 {{count} comments} one {{count} comment} few {{count} comments} other {{count} comments}}",
"Common feed": "All",
"Communities": "Communities",
"community": "community",
"Community Discussion Rules": "Community Discussion Rules",
@ -125,6 +127,7 @@
"Create post": "Create post",
"Create video": "Create video",
"Crop image": "Crop image",
"Current discussions": "Актуальные дискуссии",
"Culture": "Culture",
"Current password": "Current password",
"Date of Birth": "Date of Birth",
@ -180,6 +183,8 @@
"Feed settings": "Feed settings",
"Feedback": "Feedback",
"Fill email": "Fill email",
"Find co-authors": "Найти соавторов",
"Find collaborators": "Найдите соавторов и&nbsp;экспертов",
"Fixed": "Fixed",
"Follow": "Follow",
"Follow the topic": "Follow the topic",
@ -197,6 +202,7 @@
"Gallery name": "Gallery name",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine",
"Go to main page": "Go to main page",
"Go to discussions": "Перейти к обсуждениям",
"Group Chat": "Group Chat",
"Groups": "Groups",
"header 1": "header 1",
@ -254,10 +260,15 @@
"Italic": "Italic",
"Join": "Join",
"Join our maillist": "To receive the best postings, just enter your email",
"Join our team of authors": "Join our team of authors",
"Join our team of authors text": "Каждый месяц на&nbsp;Дискурсе публикуются десятки новых авторов. Станьте одним из&nbsp;них&nbsp;— предложите свой материал в&nbsp;журнал и&nbsp;присоединитесь к&nbsp;горизонтальной редакции",
"Join the community": "Join the community",
"Join the global community of authors!": "Join the global community of authors from all over the world!",
"journal": "journal",
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
"Join": "Join",
"Join discussions": "Присоединяйтесь к&nbsp;дискуссиям",
"Join discussions text": "Дискурс&nbsp;— свободная платформа для осмысленного общения.<br/>Здесь появятся ваши реплики, чтобы в&nbsp;любой момент вернуться к&nbsp;диалогу.",
"Just start typing...": "Just start typing...",
"keywords": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography",
"Knowledge base": "Knowledge base",
@ -313,6 +324,7 @@
"Our regular contributor": "Our regular contributor",
"Paragraphs": "Абзацев",
"Participate in the Discours: share information, join the editorial team": "Участвуйте в Дискурсе: делитесь информацией, присоединяйтесь к редакции",
"Participate in discussions": "Участвуйте в дискуссиях",
"Participating": "Participating",
"Participation": "Participation",
"Partners": "Partners",
@ -327,6 +339,9 @@
"Personal": "Personal",
"personal data usage and email notifications": "to process personal data and receive email notifications",
"Pin": "Pin",
"Placeholder feed": "Подпишитесь на&nbsp;любимые темы, авторов и&nbsp;сообщества&nbsp;— моментально узнавайте о&nbsp;новых публикациях и&nbsp;обсуждениях",
"Placeholder feedCollaborations": "На&nbsp;платформе можно писать материалы вместе. Здесь появятся публикации, в&nbsp;которые вы внесли вклад",
"Placeholder feedDiscussions": "Дискурс&nbsp;— свободная платформа для осмысленного общения. Здесь появятся все ваши реплики, чтобы в&nbsp;любой момент вернуться к&nbsp;диалогу",
"Platform Guide": "Platform Guide",
"Please check your email address": "Please check your email address",
"Please confirm your email to finish": "Confirm your email and the action will complete",

View File

@ -35,6 +35,7 @@
"All posts rating": "Рейтинг всех постов",
"all topics": "все темы",
"All topics": "Все темы",
"All": "Все",
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
"and some more authors": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?",
@ -59,6 +60,7 @@
"Be the first to rate": "Оцените первым",
"Become an author": "Стать автором",
"bold": "жирный",
"Block rules": "За что можно получить бан",
"Bold": "Жирный",
"Bookmarked": "Сохранено",
"bookmarks": "закладки",
@ -103,6 +105,7 @@
"Commenting": "Комментирование",
"Comments": "Комментарии",
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
"Common feed": "Общая лента",
"Communities": "Сообщества",
"community": "сообщество",
"Community Discussion Rules": "Правила дискуссий в сообществе",
@ -126,14 +129,14 @@
"Create an account to vote": "Создайте аккаунт, чтобы голосовать",
"Create Chat": "Создать чат",
"Create gallery": "Создать галерею",
"Create Group": "Создать группу",
"Create own feed": "Создать свою ленту",
"Create post": "Создать публикацию",
"Create video": "Создать видео",
"create_chat": "Создать чат",
"create_group": "Создать группу",
"Crop image": "Кадрировать изображение",
"Culture": "Культура",
"Current password": "Текущий пароль",
"Current discussions": "Актуальные дискуссии",
"Date of Birth": "Дата рождения",
"Decline": "Отмена",
"Delete": "Удалить",
@ -188,6 +191,8 @@
"Feed settings": "Настроить ленту",
"Feedback": "Обратная связь",
"Fill email": "Введите почту",
"Find co-authors": "Найти соавторов",
"Find collaborators": "Найдите соавторов и&nbsp;экспертов",
"Fixed": "Все поправлено",
"Follow": "Подписаться",
"Follow the topic": "Подписаться на тему",
@ -207,6 +212,7 @@
"Get notifications": "Получать уведомления",
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
"Go to main page": "Перейти на главную",
"Go to discussions": "Перейти к обсуждениям",
"Group Chat": "Общий чат",
"Groups": "Группы",
"Header": "Заголовок",
@ -266,8 +272,13 @@
"Italic": "Курсив",
"Join": "Присоединиться",
"Join our maillist": "Чтобы получать рассылку лучших публикаций, просто укажите свою почту",
"Join our team of authors": "Станьте автором",
"Join our team of authors text": "Каждый месяц на&nbsp;Дискурсе публикуются десятки новых авторов.<br/>Станьте одним из&nbsp;них&nbsp;— предложите свой материал в&nbsp;журнал и&nbsp;присоединитесь к&nbsp;горизонтальной редакции",
"Join the community": "Присоединиться к сообществу",
"Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!",
"Join": "Присоединиться",
"Join discussions": "Присоединяйтесь к&nbsp;дискуссиям",
"Join discussions text": "Дискурс&nbsp;— свободная платформа для осмысленного общения.<br/>Здесь появятся ваши реплики, чтобы в&nbsp;любой момент вернуться к&nbsp;диалогу.",
"journal": "журнал",
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
"Just start typing...": "Просто начните печатать...",
@ -328,6 +339,7 @@
"Our regular contributor": "Наш постоянный автор",
"Paragraphs": "Абзацев",
"Participate in the Discours: share information, join the editorial team": "Participate in the Discours: share information, join the editorial team",
"Participate in discussions": "Участвуйте в дискуссиях",
"Participating": "Участвовать",
"Participation": "Соучастие",
"Partners": "Партнёры",
@ -342,6 +354,9 @@
"Personal": "Личные",
"personal data usage and email notifications": "на обработку персональных данных и на получение почтовых уведомлений",
"Pin": "Закрепить",
"Placeholder feed": "Подпишитесь на&nbsp;любимые темы, авторов и&nbsp;сообщества&nbsp;— моментально узнавайте о&nbsp;новых публикациях и&nbsp;обсуждениях",
"Placeholder feedCollaborations": "На&nbsp;платформе можно писать материалы вместе. Здесь появятся публикации, в&nbsp;которые вы внесли вклад",
"Placeholder feedDiscussions": "Дискурс&nbsp;— свободная платформа для осмысленного общения. Здесь появятся все ваши реплики, чтобы в&nbsp;любой момент вернуться к&nbsp;диалогу",
"Platform Guide": "Гид по дискурсу",
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
"Please check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля",

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -18,9 +18,8 @@
.authorName {
@include font-size(4rem);
font-weight: 700;
margin-bottom: 0.2em;
margin-bottom: 1.2rem;
}
.authorAbout {
@ -429,64 +428,19 @@
}
}
.followersContainer {
.listWrapper {
max-height: 70vh;
}
.subscribersContainer {
display: flex;
flex-wrap: wrap;
font-size: 1.4rem;
margin-top: 1.5rem;
gap: 1rem;
margin-top: 0;
white-space: nowrap;
@include media-breakpoint-down(md) {
justify-content: center;
}
}
.followers {
align-items: center;
cursor: pointer;
display: inline-flex;
margin: 0 2% 1rem;
vertical-align: top;
border-bottom: unset !important;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
.followersItem {
position: relative;
&:nth-child(1) {
z-index: 2;
}
&:nth-child(2) {
z-index: 1;
}
&:not(:last-child) {
margin-right: -4px;
box-shadow: 0 0 0 1px var(--background-color);
}
}
.followsCounter {
font-weight: 500;
margin-left: 1rem;
}
&:hover {
background: none !important;
.followsCounter {
background: var(--background-color-invert);
}
}
}
.listWrapper {
max-height: 70vh;
}

View File

@ -18,6 +18,7 @@ import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { Subscribers } from '../../_shared/Subscribers'
import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic'
@ -192,61 +193,14 @@ export const AuthorCard = (props: Props) => {
<Show when={props.author.bio}>
<div class={styles.authorAbout} innerHTML={props.author.bio} />
</Show>
<Show when={props.followers?.length > 0 || props.flatFollows?.length > 0}>
<div class={styles.followersContainer}>
<Show when={props.followers && props.followers.length > 0}>
<a href="?m=followers" class={styles.followers}>
<For each={props.followers.slice(0, 3)}>
{(f: Author) => (
<Show when={f?.name}>
<Userpic
size={'XS'}
name={f?.name || ''}
userpic={f?.pic || ''}
class={styles.followersItem}
<Show when={props.followers?.length > 0 || props.following?.length > 0}>
<div class={styles.subscribersContainer}>
<Subscribers
followers={props.followers}
followersAmount={props.author?.stat?.followers}
following={props.following}
followingAmount={props.author?.stat?.authors}
/>
</Show>
)}
</For>
<div class={styles.followsCounter}>
{t('FollowersWithCount', {
count: props.followers.length ?? 0,
})}
</div>
</a>
</Show>
<Show when={props.flatFollows?.length > 0}>
<a href="?m=following" class={styles.followers}>
<For each={props.flatFollows.slice(0, 3)}>
{(f) => {
if ('name' in f) {
return (
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.followersItem} />
)
}
if ('title' in f) {
return (
<Userpic
size={'XS'}
name={f.title}
userpic={f.pic}
class={styles.followersItem}
/>
)
}
return null
}}
</For>
<div class={styles.followsCounter}>
{t('FollowsWithCount', {
count: props?.flatFollows.length ?? 0,
})}
</div>
</a>
</Show>
</div>
</Show>
</div>

View File

@ -0,0 +1,213 @@
.placeholder {
border-radius: 2.2rem;
display: flex;
@include font-size(1.4rem);
font-weight: 500;
overflow: hidden;
position: relative;
h3 {
@include font-size(2.4rem);
}
button,
.button {
align-items: center;
border-radius: 1.2rem;
display: flex;
@include font-size(1.5rem);
gap: 0.6rem;
margin-top: 3rem;
padding: 1rem 2rem;
width: 100%;
.icon {
height: 2.4rem;
width: 2.4rem;
}
}
}
.placeholder--feed-mode {
aspect-ratio: 1 / 0.8;
flex-direction: column;
text-align: center;
&:after {
bottom: 0;
content: '';
height: 20%;
left: 0;
position: absolute;
width: 100%;
.placeholder--feed & {
background: linear-gradient(to top, #171032, rgba(23, 16, 50, 0));
}
.placeholder--feedCollaborations & {
background: linear-gradient(to top, #070709, rgba(7, 7, 9, 0));
}
}
.placeholderCover {
flex: 0 100%;
width: 100%;
img {
position: absolute;
}
}
}
.placeholder--profile-mode {
min-height: 28rem;
.placeholderCover {
flex: 0 45rem;
min-width: 45rem;
order: 2;
padding: 1.6rem;
img {
height: auto;
width: 100%;
}
}
.placeholderContent {
display: flex;
flex-direction: column;
justify-content: space-between;
@include font-size(2rem);
line-height: 1.2;
padding: 3rem;
}
h3 {
@include font-size(3.8rem);
}
.button {
background: var(--background-color-invert);
color: var(--default-color-invert);
bottom: 2rem;
position: absolute;
right: 2rem;
width: auto;
.icon {
filter: invert(1);
}
}
}
.placeholderCover {
position: relative;
img {
left: 0;
height: 100%;
object-fit: cover;
width: 100%;
}
}
.placeholderContent {
padding: 1.6rem;
}
.placeholder--feed,
.placeholder--feedCollaborations {
color: var(--default-color-invert);
button,
.button {
background: var(--background-color);
color: var(--default-color);
}
}
.placeholder--feed {
background: #171032;
.placeholderCover {
img {
object-position: top;
}
}
}
.placeholder--feedCollaborations {
background: #070709;
.placeholderCover {
img {
object-position: bottom;
}
}
}
.placeholder--feedDiscussions {
background: #E9E9EE;
.placeholderCover {
padding: 2rem;
text-align: center;
img {
height: 90%;
mix-blend-mode: multiply;
object-fit: contain;
top: 10%;
}
}
button,
.button {
background: var(--background-color-invert);
color: var(--default-color-invert);
}
}
.placeholder--author {
background: #E58B72;
}
.placeholder--authorComments {
background: #E9E9EE;
.placeholderCover {
img {
mix-blend-mode: multiply;
}
}
}
.bottomLinks {
display: flex;
@include font-size(1.6rem);
gap: 4rem;
a {
border: none !important;
padding-left: 2.6rem;
position: relative;
&:hover {
.icon {
filter: invert(0);
}
}
}
.icon {
filter: invert(1);
height: 1.8rem;
left: 0;
position: absolute;
transition: filter 0.2s;
width: 1.8rem;
}
}

View File

@ -0,0 +1,120 @@
import { clsx } from 'clsx'
import { For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { Icon } from '../../_shared/Icon'
import styles from './Placeholder.module.scss'
export type PlaceholderProps = {
type: string
mode: 'feed' | 'profile'
}
export const Placeholder = (props: PlaceholderProps) => {
const { t } = useLocalize()
const { author } = useSession()
const data = {
feed: {
image: 'placeholder-feed.webp',
header: t('Feed settings'),
text: t('Placeholder feed'),
buttonLabel: author() ? t('Popular authors') : t('Create own feed'),
href: '/authors?by=followers',
},
feedCollaborations: {
image: 'placeholder-experts.webp',
header: t('Find collaborators'),
text: t('Placeholder feedCollaborations'),
buttonLabel: t('Find co-authors'),
href: '/authors?by=name',
},
feedDiscussions: {
image: 'placeholder-discussions.webp',
header: t('Participate in discussions'),
text: t('Placeholder feedDiscussions'),
buttonLabel: author() ? t('Current discussions') : t('Enter'),
href: '/feed?by=last_comment',
},
author: {
image: 'placeholder-join.webp',
header: t('Join our team of authors'),
text: t('Join our team of authors text'),
buttonLabel: t('Create post'),
href: '/create',
profileLinks: [
{
href: '/how-to-write-a-good-article',
label: t('How to write a good article'),
},
],
},
authorComments: {
image: 'placeholder-discussions.webp',
header: t('Join discussions'),
text: t('Placeholder feedDiscussions'),
buttonLabel: t('Go to discussions'),
href: '/feed?by=last_comment',
profileLinks: [
{
href: '/about/discussion-rules',
label: t('Discussion rules'),
},
{
href: '/about/discussion-rules#ban',
label: t('Block rules'),
},
],
},
}
return (
<div
class={clsx(
styles.placeholder,
styles[`placeholder--${props.type}`],
styles[`placeholder--${props.mode}-mode`],
)}
>
<div class={styles.placeholderCover}>
<img src={`/${data[props.type].image}`} />
</div>
<div class={styles.placeholderContent}>
<div>
<h3 innerHTML={data[props.type].header} />
<p innerHTML={data[props.type].text} />
</div>
<Show when={data[props.type].profileLinks}>
<div class={styles.bottomLinks}>
<For each={data[props.type].profileLinks}>
{(link) => (
<a href={link.href}>
<Icon name="link-white" class={styles.icon} />
{link.label}
</a>
)}
</For>
</div>
</Show>
<Show
when={author()}
fallback={
<a class={styles.button} href="?m=auth&mode=login">
{data[props.type].buttonLabel}
</a>
}
>
<a class={styles.button} href={data[props.type].href}>
{data[props.type].buttonLabel}
<Show when={props.mode === 'profile'}>
<Icon name="arrow-right-2" class={styles.icon} />
</Show>
</a>
</Show>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { Placeholder } from './Placeholder'

View File

@ -83,32 +83,6 @@ export const Sidebar = () => {
</span>
</a>
</li>
<li>
<a
href={getPagePath(router, 'feedBookmarks')}
class={clsx({
[styles.selected]: page().route === 'feedBookmarks',
})}
>
<span class={styles.sidebarItemName}>
<Icon name="bookmark" class={styles.icon} />
{t('Bookmarks')}
</span>
</a>
</li>
<li>
<a
href={getPagePath(router, 'feedNotifications')}
class={clsx({
[styles.selected]: page().route === 'feedNotifications',
})}
>
<span class={styles.sidebarItemName}>
<Icon name="feed-notifications" class={styles.icon} />
{t('Notifications')}
</span>
</a>
</li>
</ul>
<Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}>

View File

@ -1,6 +1,5 @@
.topicHeader {
@include font-size(1.7rem);
font-weight: 500;
padding: 2.8rem $container-padding-x 0;
text-align: center;
@ -12,10 +11,16 @@
}
}
.topicDescription {
@include font-size(1.8rem);
line-height: 1.4;
margin: 1rem 0 2rem;
}
.topicActions {
margin-top: 2.8rem;
.write {
.writeControl {
display: inline-flex;
align-items: center;
justify-content: center;
@ -23,13 +28,38 @@
min-width: 64px;
font-size: 17px;
padding: 8px 16px;
background: var(--background-color-invert);
color: var(--default-color-invert);
border: none;
border: 1px solid #f7f7f7;
background: #f7f7f7;
color: var(--default-color);
font-weight: 500;
border-radius: 2px;
cursor: pointer;
margin: 0 1.2rem 1em;
white-space: nowrap;
}
.followControl,
.writeControl {
border-radius: 0.8rem;
}
}
.topicDetails {
align-items: flex-start;
display: flex;
flex-wrap: wrap;
font-size: 1.4rem;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
}
.topicDetailsItem {
align-items: center;
display: flex;
margin-right: 1rem;
white-space: nowrap;
}
.topicDetailsIcon {
display: block;
}

View File

@ -1,4 +1,4 @@
import type { Topic } from '../../graphql/schema/core.gen'
import type { Author, Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx'
import { Show, createEffect, createSignal } from 'solid-js'
@ -9,10 +9,14 @@ import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/schema/core.gen'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Subscribers } from '../_shared/Subscribers'
import styles from './Full.module.scss'
type Props = {
topic: Topic
followers?: Author[]
authors?: Author[]
}
export const FullTopic = (props: Props) => {
@ -39,19 +43,39 @@ export const FullTopic = (props: Props) => {
return (
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
<h1>#{props.topic?.title}</h1>
<p innerHTML={props.topic?.body} />
<p class={styles.topicDescription} innerHTML={props.topic?.body} />
<div class={styles.topicDetails}>
<Show when={props.topic?.stat}>
<div class={styles.topicDetailsItem}>
<Icon name="feed-all" class={styles.topicDetailsIcon} />
{t('PublicationsWithCount', {
count: props.topic?.stat.shouts ?? 0,
})}
</div>
</Show>
<Subscribers
followers={props.followers}
followersAmount={props.topic?.stat?.followers}
following={props.authors}
followingAmount={props.topic?.stat?.authors}
/>
</div>
<div class={clsx(styles.topicActions)}>
<Button
variant="primary"
onClick={handleFollowClick}
value={followed() ? t('Unfollow the topic') : t('Follow the topic')}
class={styles.followControl}
/>
<a class={styles.write} href={`/create/?topicId=${props.topic?.id}`}>
<a class={styles.writeControl} href={`/create/?topicId=${props.topic?.id}`}>
{t('Write about the topic')}
</a>
</div>
<Show when={props.topic?.pic}>
<img src={props.topic.pic} alt={props.topic?.title} />
<img src={props.topic?.pic} alt={props.topic?.title} />
</Show>
</div>
)

View File

@ -27,6 +27,7 @@ import { Loading } from '../../_shared/Loading'
import { MODALS, hideModal } from '../../../stores/ui'
import { byCreated } from '../../../utils/sortby'
import stylesArticle from '../../Article/Article.module.scss'
import { Placeholder } from '../../Feed/Placeholder'
import styles from './Author.module.scss'
type Props = {
@ -250,6 +251,10 @@ export const AuthorView = (props: Props) => {
</div>
</Match>
<Match when={getPage().route === 'authorComments'}>
<div class="wide-container">
<Placeholder type={getPage().route} mode="profile" />
</div>
<div class="wide-container">
<div class="row">
<div class="col-md-20 col-lg-18">
@ -270,6 +275,15 @@ export const AuthorView = (props: Props) => {
</div>
</Match>
<Match when={getPage().route === 'author'}>
<Show
when={session()?.user?.app_data?.profile?.slug === props.authorSlug && !sortedArticles().length}
>
<div class="wide-container">
<Placeholder type={getPage().route} mode="profile" />
</div>
</Show>
<Show when={sortedArticles().length > 0}>
<Show when={sortedArticles().length === 1}>
<Row1 article={sortedArticles()[0]} noauthor={true} nodate={true} />
</Show>
@ -311,6 +325,7 @@ export const AuthorView = (props: Props) => {
</button>
</p>
</Show>
</Show>
</Match>
</Switch>
</div>

View File

@ -175,6 +175,7 @@
-webkit-line-clamp: 1;
a {
border: none;
color: rgb(0 0 0 / 65%);
&:hover {

View File

@ -20,6 +20,7 @@ import { getShareUrl } from '../../Article/SharePopup'
import { AuthorBadge } from '../../Author/AuthorBadge'
import { AuthorLink } from '../../Author/AuthorLink'
import { ArticleCard } from '../../Feed/ArticleCard'
import { Placeholder } from '../../Feed/Placeholder'
import { Sidebar } from '../../Feed/Sidebar'
import { Modal } from '../../Nav/Modal'
import { DropDown } from '../../_shared/DropDown'
@ -100,7 +101,7 @@ export const FeedView = (props: Props) => {
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
const { session } = useSession()
const { author, session } = useSession()
const { loadReactionsBy } = useReactions()
const { sortedArticles } = useArticlesStore()
const { topTopics } = useTopics()
@ -238,6 +239,10 @@ export const FeedView = (props: Props) => {
</div>
<div class="col-md-12 offset-xl-1">
<Show
when={author() || !sortedArticles().length}
fallback={<Placeholder type={page().route} mode="feed" />}
>
<div class={styles.filtersContainer}>
<ul class={clsx('view-switcher', styles.feedFilter)}>
<li
@ -341,6 +346,7 @@ export const FeedView = (props: Props) => {
</p>
</Show>
</Show>
</Show>
</div>
<aside class={clsx('col-md-7 col-xl-6 offset-xl-1', styles.feedAside)}>

View File

@ -1,4 +1,4 @@
import { LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
@ -33,6 +33,7 @@ interface Props {
topic: Topic
shouts: Shout[]
topicSlug: string
followers?: Author[]
}
export const PRERENDERED_ARTICLES_COUNT = 28
@ -56,6 +57,11 @@ export const TopicView = (props: Props) => {
setTopic(topics[props.topicSlug])
}
})
const [followers, setFollowers] = createSignal<Author[]>(props.followers || [])
const loadTopicFollowers = async () => {
const result = await apiClient.getTopicFollowers({ slug: props.topicSlug })
setFollowers(result)
}
const loadFavoriteTopArticles = async (topic: string) => {
const options: LoadShoutsOptions = {
@ -81,13 +87,29 @@ export const TopicView = (props: Props) => {
setReactedTopMonthArticles(result)
}
const [topicAuthors, setTopicAuthors] = createSignal<Author[]>([])
const loadTopicAuthors = async () => {
const by: AuthorsBy = { topic: props.topicSlug }
const result = await apiClient.loadAuthorsBy({ by })
setTopicAuthors(result)
}
const loadRandom = () => {
loadFavoriteTopArticles(topic()?.slug)
loadReactedTopMonthArticles(topic()?.slug)
}
createEffect(on(topic, loadRandom, { defer: true }))
createEffect(
on(
() => topic()?.id,
(_) => {
loadTopicFollowers()
loadTopicAuthors()
loadRandom()
},
{ defer: true },
),
)
const title = createMemo(
() =>
@ -152,7 +174,7 @@ export const TopicView = (props: Props) => {
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={title()} />
<Meta name="twitter:description" content={description()} />
<FullTopic topic={topic()} />
<FullTopic topic={topic()} followers={followers()} authors={topicAuthors()} />
<div class="wide-container">
<div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="col-md-16">

View File

@ -0,0 +1,50 @@
.subscribers {
align-items: center;
cursor: pointer;
display: inline-flex;
margin: 0 1rem 0 0;
vertical-align: top;
border-bottom: unset !important;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
.subscribersItem {
position: relative;
&:nth-child(1) {
z-index: 2;
}
&:nth-child(2) {
z-index: 1;
}
&:not(:last-child) {
margin-right: -4px;
box-shadow: 0 0 0 1px var(--background-color);
}
}
.subscribersCounter {
font-weight: 500;
}
&:hover {
background: none !important;
.subscribersCounter {
background: var(--background-color-invert);
}
}
}
.subscribersList {
display: flex;
margin-right: 0.6rem;
}

View File

@ -0,0 +1,67 @@
import { For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Author, Topic } from '../../../graphql/schema/core.gen'
import { Userpic } from '../../Author/Userpic'
import styles from './Subscribers.module.scss'
type Props = {
followers?: Author[]
followersAmount?: number
following?: Array<Author | Topic>
followingAmount?: number
}
export const Subscribers = (props: Props) => {
const { t } = useLocalize()
return (
<>
<a href="?m=followers" class={styles.subscribers}>
<Show when={props.followers && props.followers.length > 0}>
<div class={styles.subscribersList}>
<For each={props.followers.slice(0, 3)}>
{(f) => <Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />}
</For>
</div>
</Show>
<div class={styles.subscribersCounter}>
{t('SubscriberWithCount', {
count: props.followersAmount || props.followers.length || 0,
})}
</div>
</a>
<a href="?m=following" class={styles.subscribers}>
<Show when={props.following && props.following.length > 0}>
<div class={styles.subscribersList}>
<For each={props.following.slice(0, 3)}>
{(f) => {
if ('name' in f) {
return (
<Userpic size={'XS'} name={f.name} userpic={f.pic} class={styles.subscribersItem} />
)
}
if ('title' in f) {
return (
<Userpic size={'XS'} name={f.title} userpic={f.pic} class={styles.subscribersItem} />
)
}
return null
}}
</For>
</div>
</Show>
<div class={styles.subscribersCounter}>
{t('SubscriptionWithCount', {
count: props.followingAmount || props.following?.length || 0,
})}
</div>
</a>
</>
)
}

View File

@ -0,0 +1 @@
export { Subscribers } from './Subscribers'

View File

@ -5,6 +5,7 @@ import type {
LoadShoutsOptions,
MutationDelete_ShoutArgs,
ProfileInput,
QueryGet_Topic_FollowersArgs,
QueryLoad_Authors_ByArgs,
QueryLoad_Shouts_Random_TopArgs,
QueryLoad_Shouts_SearchArgs,
@ -44,6 +45,7 @@ import authorsAll from '../query/core/authors-all'
import authorsLoadBy from '../query/core/authors-load-by'
import reactionsLoadBy from '../query/core/reactions-load-by'
import topicBySlug from '../query/core/topic-by-slug'
import topicFollowers from '../query/core/topic-followers'
import topicsAll from '../query/core/topics-all'
import topicsRandomQuery from '../query/core/topics-random'
@ -129,6 +131,11 @@ export const apiClient = {
return response.data.get_author_followers
},
getTopicFollowers: async ({ slug }: QueryGet_Topic_FollowersArgs): Promise<Author[]> => {
const response = await publicGraphQLClient.query(topicFollowers, { slug }).toPromise()
return response.data.get_topic_followers
},
getAuthorFollows: async (params: {
slug?: string
author_id?: number

View File

@ -0,0 +1,25 @@
import { gql } from '@urql/core'
export default gql`
query TopicFollowersQuery($slug: String) {
get_topic_followers(slug: $slug) {
id
slug
name
bio
about
pic
# communities
links
created_at
last_seen
stat {
shouts
authors
followers
rating
comments
}
}
}
`

View File

@ -40,7 +40,7 @@ export const DiscussionRulesPage = () => {
людей рождается истина.
</p>
<h3>За&nbsp;что можно получить дырку в&nbsp;карме и&nbsp;выиграть бан в&nbsp;сообществе</h3>
<h3 id="ban">За&nbsp;что можно получить дырку в&nbsp;карме и&nbsp;выиграть бан в&nbsp;сообществе</h3>
<ol>
<li>
<p>

View File

@ -1,6 +1,5 @@
import { Match, Switch, createEffect, on, onCleanup } from 'solid-js'
import { createEffect, on, onCleanup } from 'solid-js'
import { AuthGuard } from '../components/AuthGuard'
import { Feed } from '../components/Views/Feed'
import { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize'
@ -32,16 +31,7 @@ export const FeedPage = () => {
return (
<PageLayout title={t('Feed')}>
<ReactionsProvider>
<Switch fallback={<Feed loadShouts={handleFeedLoadShouts} />}>
<Match when={page().route === 'feed'}>
<Feed loadShouts={handleFeedLoadShouts} />
</Match>
<Match when={page().route === 'feedMy'}>
<AuthGuard>
<Feed loadShouts={handleMyFeedLoadShouts} />
</AuthGuard>
</Match>
</Switch>
<Feed loadShouts={page().route === 'feedMy' ? handleMyFeedLoadShouts : handleFeedLoadShouts} />
</ReactionsProvider>
</PageLayout>
)