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

View File

@ -35,6 +35,7 @@
"All posts rating": "Рейтинг всех постов", "All posts rating": "Рейтинг всех постов",
"all topics": "все темы", "all topics": "все темы",
"All topics": "Все темы", "All topics": "Все темы",
"All": "Все",
"Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.", "Almost done! Check your email.": "Почти готово! Осталось подтвердить вашу почту.",
"and some more authors": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}", "and some more authors": "{restUsersCount, plural, =0 {} one { и ещё 1 пользователя} few { и ещё {restUsersCount} пользователей} other { и ещё {restUsersCount} пользователей}}",
"Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?", "Are you sure you want to delete this comment?": "Уверены, что хотите удалить этот комментарий?",
@ -59,6 +60,7 @@
"Be the first to rate": "Оцените первым", "Be the first to rate": "Оцените первым",
"Become an author": "Стать автором", "Become an author": "Стать автором",
"bold": "жирный", "bold": "жирный",
"Block rules": "За что можно получить бан",
"Bold": "Жирный", "Bold": "Жирный",
"Bookmarked": "Сохранено", "Bookmarked": "Сохранено",
"bookmarks": "закладки", "bookmarks": "закладки",
@ -103,6 +105,7 @@
"Commenting": "Комментирование", "Commenting": "Комментирование",
"Comments": "Комментарии", "Comments": "Комментарии",
"CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}", "CommentsWithCount": "{count, plural, =0 {{count} комментариев} one {{count} комментарий} few {{count} комментария} other {{count} комментариев}}",
"Common feed": "Общая лента",
"Communities": "Сообщества", "Communities": "Сообщества",
"community": "сообщество", "community": "сообщество",
"Community Discussion Rules": "Правила дискуссий в сообществе", "Community Discussion Rules": "Правила дискуссий в сообществе",
@ -126,14 +129,14 @@
"Create an account to vote": "Создайте аккаунт, чтобы голосовать", "Create an account to vote": "Создайте аккаунт, чтобы голосовать",
"Create Chat": "Создать чат", "Create Chat": "Создать чат",
"Create gallery": "Создать галерею", "Create gallery": "Создать галерею",
"Create Group": "Создать группу", "Create own feed": "Создать свою ленту",
"Create post": "Создать публикацию", "Create post": "Создать публикацию",
"Create video": "Создать видео", "Create video": "Создать видео",
"create_chat": "Создать чат", "create_chat": "Создать чат",
"create_group": "Создать группу", "create_group": "Создать группу",
"Crop image": "Кадрировать изображение", "Crop image": "Кадрировать изображение",
"Culture": "Культура", "Culture": "Культура",
"Current password": "Текущий пароль", "Current discussions": "Актуальные дискуссии",
"Date of Birth": "Дата рождения", "Date of Birth": "Дата рождения",
"Decline": "Отмена", "Decline": "Отмена",
"Delete": "Удалить", "Delete": "Удалить",
@ -188,6 +191,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": "Подписаться на тему",
@ -207,6 +212,7 @@
"Get notifications": "Получать уведомления", "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": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "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": "Заголовок", "Header": "Заголовок",
@ -266,8 +272,13 @@
"Italic": "Курсив", "Italic": "Курсив",
"Join": "Присоединиться", "Join": "Присоединиться",
"Join our maillist": "Чтобы получать рассылку лучших публикаций, просто укажите свою почту", "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 community": "Присоединиться к сообществу",
"Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!", "Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!",
"Join": "Присоединиться",
"Join discussions": "Присоединяйтесь к&nbsp;дискуссиям",
"Join discussions text": "Дискурс&nbsp;— свободная платформа для осмысленного общения.<br/>Здесь появятся ваши реплики, чтобы в&nbsp;любой момент вернуться к&nbsp;диалогу.",
"journal": "журнал", "journal": "журнал",
"jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.", "jpg, .png, max. 10 mb.": "jpg, .png, макс. 10 мб.",
"Just start typing...": "Просто начните печатать...", "Just start typing...": "Просто начните печатать...",
@ -328,6 +339,7 @@
"Our regular contributor": "Наш постоянный автор", "Our regular contributor": "Наш постоянный автор",
"Paragraphs": "Абзацев", "Paragraphs": "Абзацев",
"Participate in the Discours: share information, join the editorial team": "Participate in the Discours: share information, join the editorial team", "Participate in the Discours: share information, join the editorial team": "Participate in the Discours: share information, join the editorial team",
"Participate in discussions": "Участвуйте в дискуссиях",
"Participating": "Участвовать", "Participating": "Участвовать",
"Participation": "Соучастие", "Participation": "Соучастие",
"Partners": "Партнёры", "Partners": "Партнёры",
@ -342,6 +354,9 @@
"Personal": "Личные", "Personal": "Личные",
"personal data usage and email notifications": "на обработку персональных данных и на получение почтовых уведомлений", "personal data usage and 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 check your inbox! We have sent a password reset link.": "Пожалуйста, проверьте свою почту, мы отправили вам письмо со ссылкой для сброса пароля", "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 { .authorName {
@include font-size(4rem); @include font-size(4rem);
font-weight: 700; font-weight: 700;
margin-bottom: 0.2em; margin-bottom: 1.2rem;
} }
.authorAbout { .authorAbout {
@ -429,64 +428,19 @@
} }
} }
.followersContainer { .listWrapper {
max-height: 70vh;
}
.subscribersContainer {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.4rem; font-size: 1.4rem;
margin-top: 1.5rem; gap: 1rem;
margin-top: 0;
white-space: nowrap;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
justify-content: center; 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 { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { Subscribers } from '../../_shared/Subscribers'
import { AuthorBadge } from '../AuthorBadge' import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
@ -192,61 +193,14 @@ export const AuthorCard = (props: Props) => {
<Show when={props.author.bio}> <Show when={props.author.bio}>
<div class={styles.authorAbout} innerHTML={props.author.bio} /> <div class={styles.authorAbout} innerHTML={props.author.bio} />
</Show> </Show>
<Show when={props.followers?.length > 0 || props.flatFollows?.length > 0}> <Show when={props.followers?.length > 0 || props.following?.length > 0}>
<div class={styles.followersContainer}> <div class={styles.subscribersContainer}>
<Show when={props.followers && props.followers.length > 0}> <Subscribers
<a href="?m=followers" class={styles.followers}> followers={props.followers}
<For each={props.followers.slice(0, 3)}> followersAmount={props.author?.stat?.followers}
{(f: Author) => ( following={props.following}
<Show when={f?.name}> followingAmount={props.author?.stat?.authors}
<Userpic
size={'XS'}
name={f?.name || ''}
userpic={f?.pic || ''}
class={styles.followersItem}
/> />
</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> </div>
</Show> </Show>
</div> </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> </span>
</a> </a>
</li> </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> </ul>
<Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}> <Show when={follows?.authors?.length > 0 || follows?.topics?.length > 0}>

View File

@ -1,6 +1,5 @@
.topicHeader { .topicHeader {
@include font-size(1.7rem); font-weight: 500;
padding: 2.8rem $container-padding-x 0; padding: 2.8rem $container-padding-x 0;
text-align: center; text-align: center;
@ -12,10 +11,16 @@
} }
} }
.topicDescription {
@include font-size(1.8rem);
line-height: 1.4;
margin: 1rem 0 2rem;
}
.topicActions { .topicActions {
margin-top: 2.8rem; margin-top: 2.8rem;
.write { .writeControl {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -23,13 +28,38 @@
min-width: 64px; min-width: 64px;
font-size: 17px; font-size: 17px;
padding: 8px 16px; padding: 8px 16px;
background: var(--background-color-invert); border: 1px solid #f7f7f7;
color: var(--default-color-invert); background: #f7f7f7;
border: none; color: var(--default-color);
font-weight: 500; font-weight: 500;
border-radius: 2px;
cursor: pointer; cursor: pointer;
margin: 0 1.2rem 1em; margin: 0 1.2rem 1em;
white-space: nowrap; 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 { clsx } from 'clsx'
import { Show, createEffect, createSignal } from 'solid-js' import { Show, createEffect, createSignal } from 'solid-js'
@ -9,10 +9,14 @@ import { useSession } from '../../context/session'
import { FollowingEntity } from '../../graphql/schema/core.gen' import { FollowingEntity } from '../../graphql/schema/core.gen'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Subscribers } from '../_shared/Subscribers'
import styles from './Full.module.scss' import styles from './Full.module.scss'
type Props = { type Props = {
topic: Topic topic: Topic
followers?: Author[]
authors?: Author[]
} }
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
@ -39,19 +43,39 @@ export const FullTopic = (props: Props) => {
return ( return (
<div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}> <div class={clsx(styles.topicHeader, 'col-md-16 col-lg-12 offset-md-4 offset-lg-6')}>
<h1>#{props.topic?.title}</h1> <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)}> <div class={clsx(styles.topicActions)}>
<Button <Button
variant="primary" variant="primary"
onClick={handleFollowClick} onClick={handleFollowClick}
value={followed() ? t('Unfollow the topic') : t('Follow the topic')} 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')} {t('Write about the topic')}
</a> </a>
</div> </div>
<Show when={props.topic?.pic}> <Show when={props.topic?.pic}>
<img src={props.topic.pic} alt={props.topic?.title} /> <img src={props.topic?.pic} alt={props.topic?.title} />
</Show> </Show>
</div> </div>
) )

View File

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

View File

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

View File

@ -20,6 +20,7 @@ import { getShareUrl } from '../../Article/SharePopup'
import { AuthorBadge } from '../../Author/AuthorBadge' import { AuthorBadge } from '../../Author/AuthorBadge'
import { AuthorLink } from '../../Author/AuthorLink' import { AuthorLink } from '../../Author/AuthorLink'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import { Placeholder } from '../../Feed/Placeholder'
import { Sidebar } from '../../Feed/Sidebar' import { Sidebar } from '../../Feed/Sidebar'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { DropDown } from '../../_shared/DropDown' import { DropDown } from '../../_shared/DropDown'
@ -100,7 +101,7 @@ export const FeedView = (props: Props) => {
const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>() const { page, searchParams, changeSearchParams } = useRouter<FeedSearchParams>()
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false) const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
const { session } = useSession() const { author, session } = useSession()
const { loadReactionsBy } = useReactions() const { loadReactionsBy } = useReactions()
const { sortedArticles } = useArticlesStore() const { sortedArticles } = useArticlesStore()
const { topTopics } = useTopics() const { topTopics } = useTopics()
@ -238,6 +239,10 @@ export const FeedView = (props: Props) => {
</div> </div>
<div class="col-md-12 offset-xl-1"> <div class="col-md-12 offset-xl-1">
<Show
when={author() || !sortedArticles().length}
fallback={<Placeholder type={page().route} mode="feed" />}
>
<div class={styles.filtersContainer}> <div class={styles.filtersContainer}>
<ul class={clsx('view-switcher', styles.feedFilter)}> <ul class={clsx('view-switcher', styles.feedFilter)}>
<li <li
@ -341,6 +346,7 @@ export const FeedView = (props: Props) => {
</p> </p>
</Show> </Show>
</Show> </Show>
</Show>
</div> </div>
<aside class={clsx('col-md-7 col-xl-6 offset-xl-1', styles.feedAside)}> <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 { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
@ -33,6 +33,7 @@ interface Props {
topic: Topic topic: Topic
shouts: Shout[] shouts: Shout[]
topicSlug: string topicSlug: string
followers?: Author[]
} }
export const PRERENDERED_ARTICLES_COUNT = 28 export const PRERENDERED_ARTICLES_COUNT = 28
@ -56,6 +57,11 @@ export const TopicView = (props: Props) => {
setTopic(topics[props.topicSlug]) 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 loadFavoriteTopArticles = async (topic: string) => {
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
@ -81,13 +87,29 @@ export const TopicView = (props: Props) => {
setReactedTopMonthArticles(result) 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 = () => { const loadRandom = () => {
loadFavoriteTopArticles(topic()?.slug) loadFavoriteTopArticles(topic()?.slug)
loadReactedTopMonthArticles(topic()?.slug) loadReactedTopMonthArticles(topic()?.slug)
} }
createEffect(on(topic, loadRandom, { defer: true })) createEffect(
on(
() => topic()?.id,
(_) => {
loadTopicFollowers()
loadTopicAuthors()
loadRandom()
},
{ defer: true },
),
)
const title = createMemo( const title = createMemo(
() => () =>
@ -152,7 +174,7 @@ export const TopicView = (props: Props) => {
<Meta name="twitter:card" content="summary_large_image" /> <Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={title()} /> <Meta name="twitter:title" content={title()} />
<Meta name="twitter:description" content={description()} /> <Meta name="twitter:description" content={description()} />
<FullTopic topic={topic()} /> <FullTopic topic={topic()} followers={followers()} authors={topicAuthors()} />
<div class="wide-container"> <div class="wide-container">
<div class={clsx(styles.groupControls, 'row group__controls')}> <div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="col-md-16"> <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, LoadShoutsOptions,
MutationDelete_ShoutArgs, MutationDelete_ShoutArgs,
ProfileInput, ProfileInput,
QueryGet_Topic_FollowersArgs,
QueryLoad_Authors_ByArgs, QueryLoad_Authors_ByArgs,
QueryLoad_Shouts_Random_TopArgs, QueryLoad_Shouts_Random_TopArgs,
QueryLoad_Shouts_SearchArgs, QueryLoad_Shouts_SearchArgs,
@ -44,6 +45,7 @@ import authorsAll from '../query/core/authors-all'
import authorsLoadBy from '../query/core/authors-load-by' import authorsLoadBy from '../query/core/authors-load-by'
import reactionsLoadBy from '../query/core/reactions-load-by' import reactionsLoadBy from '../query/core/reactions-load-by'
import topicBySlug from '../query/core/topic-by-slug' import topicBySlug from '../query/core/topic-by-slug'
import topicFollowers from '../query/core/topic-followers'
import topicsAll from '../query/core/topics-all' import topicsAll from '../query/core/topics-all'
import topicsRandomQuery from '../query/core/topics-random' import topicsRandomQuery from '../query/core/topics-random'
@ -129,6 +131,11 @@ export const apiClient = {
return response.data.get_author_followers 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: { getAuthorFollows: async (params: {
slug?: string slug?: string
author_id?: number 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> </p>
<h3>За&nbsp;что можно получить дырку в&nbsp;карме и&nbsp;выиграть бан в&nbsp;сообществе</h3> <h3 id="ban">За&nbsp;что можно получить дырку в&nbsp;карме и&nbsp;выиграть бан в&nbsp;сообществе</h3>
<ol> <ol>
<li> <li>
<p> <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 { Feed } from '../components/Views/Feed'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
@ -32,16 +31,7 @@ export const FeedPage = () => {
return ( return (
<PageLayout title={t('Feed')}> <PageLayout title={t('Feed')}>
<ReactionsProvider> <ReactionsProvider>
<Switch fallback={<Feed loadShouts={handleFeedLoadShouts} />}> <Feed loadShouts={page().route === 'feedMy' ? handleMyFeedLoadShouts : handleFeedLoadShouts} />
<Match when={page().route === 'feed'}>
<Feed loadShouts={handleFeedLoadShouts} />
</Match>
<Match when={page().route === 'feedMy'}>
<AuthGuard>
<Feed loadShouts={handleMyFeedLoadShouts} />
</AuthGuard>
</Match>
</Switch>
</ReactionsProvider> </ReactionsProvider>
</PageLayout> </PageLayout>
) )