Feature/profile improvements (#255)

* Show social in profile
* change profile view
* fix facebook
This commit is contained in:
Ilya Y 2023-10-12 17:28:00 +03:00 committed by GitHub
parent c9febc75ea
commit 13b92c2201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 370 additions and 314 deletions

View File

@ -413,5 +413,7 @@
"video": "video", "video": "video",
"view": "view", "view": "view",
"zine": "zine", "zine": "zine",
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}" "SubscriptionWithPlurals": "{count, plural, =0 {no subscriptions} one {{count} subscription} other {{count} subscriptions}",
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}",
"Edit profile": "Edit profile"
} }

View File

@ -430,14 +430,14 @@
"subscriber": "подписчик", "subscriber": "подписчик",
"subscriber_rp": "подписчика", "subscriber_rp": "подписчика",
"subscribers": "подписчиков", "subscribers": "подписчиков",
"subscription": "подписка",
"subscription_rp": "подписки",
"subscriptions": "подписок",
"terms of use": "правилами пользования сайтом", "terms of use": "правилами пользования сайтом",
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"video": "видео", "video": "видео",
"view": "просмотр", "view": "просмотр",
"zine": "журнал", "zine": "журнал",
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}" "SubscriberWithCount": "{count, plural, =0 {нет подписчиков} one {{count} подписчик} few {{count} подписчика} other {{count} подписчиков}}",
"SubscriptionWithCount": "{count, plural, =0 {нет подписок} one {{count} подписка} few {{count} подписки} other {{count} подписок}}",
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}",
"Edit profile": "Редактировать профиль"
} }

View File

@ -124,7 +124,45 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
a { .button {
padding-left: 2rem;
padding-right: 2rem;
margin-right: 0.5em;
&:first-of-type {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:hover {
.buttonUnfollowLabel {
display: block;
}
.buttonSubscribedLabel {
display: none;
}
}
.buttonUnfollowLabel {
display: none;
}
}
}
.authorSubscribeSocialLabel {
display: none;
}
.authorSubscribeSocial {
align-items: center;
display: flex;
margin: 2rem 0;
.socialLink {
border: none; border: none;
display: inline-block; display: inline-block;
height: 24px; height: 24px;
@ -165,251 +203,213 @@
position: absolute; position: absolute;
} }
} }
}
a[href*='facebook.com/'] { &[href*='facebook.com/'] {
&::before { &::before {
background-image: url(/icons/user-link-facebook.svg); background-image: url(/icons/user-link-facebook.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='twitter.com/'] {
&::before {
background-image: url(/icons/user-link-twitter.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='telegram.com/'] {
&::before {
background-image: url(/icons/user-link-telegram.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='vk.cc/'],
a[href*='vk.com/'] {
&::before {
background-image: url(/icons/user-link-vk.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='tumblr.com/'] {
&::before {
background-image: url(/icons/user-link-tumblr.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='instagram.com/'] {
&::before {
background-image: url(/icons/user-link-instagram.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='behance.net/'] {
&::before {
background-image: url(/icons/user-link-behance.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='dribbble.com/'] {
&::before {
background-image: url(/icons/user-link-dribbble.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='github.com/'] {
&::before {
background-image: url(/icons/user-link-github.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='linkedin.com/'] {
&::before {
background-image: url(/icons/user-link-linkedin.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='medium.com/'] {
&::before {
background-image: url(/icons/user-link-medium.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='ok.ru/'] {
&::before {
background-image: url(/icons/user-link-ok.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='pinterest.com/'] {
&::before {
background-image: url(/icons/user-link-pinterest.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='reddit.com/'] {
&::before {
background-image: url(/icons/user-link-reddit.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='tiktok.com/'] {
&::before {
background-image: url(/icons/user-link-tiktok.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='youtube.com/'],
a[href*='youtu.be/'] {
&::before {
background-image: url(/icons/user-link-youtube.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
a[href*='dzen.ru/'] {
&::before {
background-image: url(/icons/user-link-dzen.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
.button {
padding-left: 2rem;
padding-right: 2rem;
margin-right: 0.5em;
&:first-of-type {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:hover {
.buttonUnfollowLabel {
display: block;
} }
.buttonSubscribedLabel { &:hover {
display: none; .authorSubscribeSocialLabel {
display: none;
}
} }
} }
.buttonUnfollowLabel { &[href*='twitter.com/'] {
display: none; &::before {
background-image: url(/icons/user-link-twitter.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='telegram.com/'] {
&::before {
background-image: url(/icons/user-link-telegram.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='vk.cc/'],
&[href*='vk.com/'] {
&::before {
background-image: url(/icons/user-link-vk.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='tumblr.com/'] {
&::before {
background-image: url(/icons/user-link-tumblr.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='instagram.com/'] {
&::before {
background-image: url(/icons/user-link-instagram.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='behance.net/'] {
&::before {
background-image: url(/icons/user-link-behance.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='dribbble.com/'] {
&::before {
background-image: url(/icons/user-link-dribbble.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='github.com/'] {
&::before {
background-image: url(/icons/user-link-github.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='linkedin.com/'] {
&::before {
background-image: url(/icons/user-link-linkedin.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='medium.com/'] {
&::before {
background-image: url(/icons/user-link-medium.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='ok.ru/'] {
&::before {
background-image: url(/icons/user-link-ok.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='pinterest.com/'] {
&::before {
background-image: url(/icons/user-link-pinterest.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='reddit.com/'] {
&::before {
background-image: url(/icons/user-link-reddit.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='tiktok.com/'] {
&::before {
background-image: url(/icons/user-link-tiktok.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='youtube.com/'],
&[href*='youtu.be/'] {
&::before {
background-image: url(/icons/user-link-youtube.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
}
&[href*='dzen.ru/'] {
&::before {
background-image: url(/icons/user-link-dzen.svg);
}
&:hover {
.authorSubscribeSocialLabel {
display: none;
}
}
} }
} }
}
.authorSubscribeSocialLabel {
display: none;
}
.authorSubscribeSocial {
align-items: center;
display: flex;
margin: 0 0.8rem 2rem 0;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
flex: 1 100%; flex: 1 100%;
@ -626,6 +626,7 @@
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.4rem; font-size: 1.4rem;
margin-top: 0.5rem; margin-top: 0.5rem;
gap: 1rem;
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
justify-content: center; justify-content: center;

View File

@ -2,7 +2,7 @@ import type { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import styles from './AuthorCard.module.scss' import styles from './AuthorCard.module.scss'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Match, Show, Switch } from 'solid-js'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -15,11 +15,15 @@ import { useLocalize } from '../../../context/localize'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'
import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { AuthorBadge } from '../AuthorBadge' import { AuthorBadge } from '../AuthorBadge'
import { TopicBadge } from '../../Topic/TopicBadge' import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import stylesHeader from '../../Nav/Header/Header.module.scss'
import { getDescription } from '../../../utils/meta'
import { Popover } from '../../_shared/Popover'
type Props = { type Props = {
caption?: string caption?: string
@ -43,6 +47,7 @@ type Props = {
following?: Array<Author | Topic> following?: Array<Author | Topic>
showPublicationsCounter?: boolean showPublicationsCounter?: boolean
hideBio?: boolean hideBio?: boolean
isCurrentUser?: boolean
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
@ -130,7 +135,6 @@ export const AuthorCard = (props: Props) => {
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) { if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) {
setUserpicUrl(props.author.userpic.replace('100x', '500x500')) setUserpicUrl(props.author.userpic.replace('100x', '500x500'))
} }
return ( return (
<> <>
<div <div
@ -219,74 +223,89 @@ export const AuthorCard = (props: Props) => {
} }
> >
<div class={styles.subscribersContainer}> <div class={styles.subscribersContainer}>
<Show when={props.followers && props.followers.length > 0}> <Switch>
<div <Match when={props.followers && props.followers.length > 0 && !props.isCurrentUser}>
class={styles.subscribers} <div
onClick={() => { class={styles.subscribers}
redirectPage(router, 'authorFollowers', { slug: props.author.slug }) onClick={() => {
showModal('followers') redirectPage(router, 'authorFollowers', { slug: props.author.slug })
}} showModal('followers')
>
<For each={props.followers.slice(0, 3)}>
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
</For>
<div class={styles.subscribersCounter}>
{props.followers.length}&nbsp;
{getNumeralsDeclension(props.followers.length, [
t('subscriber'),
t('subscriber_rp'),
t('subscribers')
])}
</div>
</div>
</Show>
<Show when={props.following && props.following.length > 0}>
<div
class={styles.subscribers}
onClick={() => {
redirectPage(router, 'authorFollowing', { slug: props.author.slug })
showModal('following')
}}
>
<For each={props.following.slice(0, 3)}>
{(f) => {
if ('name' in f) {
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
} else if ('title' in f) {
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
}
return null
}} }}
</For> >
<div class={styles.subscribersCounter}> <For each={props.followers.slice(0, 3)}>
{props.following.length}&nbsp; {(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
{getNumeralsDeclension(props.following.length, [ </For>
t('subscription'), <div class={styles.subscribersCounter}>
t('subscription_rp'), {t('SubscriptionWithCount', { count: props.followers.length })}
t('subscriptions') </div>
])}
</div> </div>
</div> </Match>
</Show> <Match when={props.followers && props.followers.length > 0 && props.isCurrentUser}>
<Button
variant="secondary"
onClick={() => redirectPage(router, 'profileSettings')}
value={t('Edit profile')}
/>
</Match>
</Switch>
<Switch>
<Match when={props.following && props.following.length > 0 && !props.isCurrentUser}>
<div
class={styles.subscribers}
onClick={() => {
redirectPage(router, 'authorFollowing', { slug: props.author.slug })
showModal('following')
}}
>
<For each={props.following.slice(0, 3)}>
{(f) => {
if ('name' in f) {
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
} else if ('title' in f) {
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
}
return null
}}
</For>
<div class={styles.subscribersCounter}>
{t('SubscriberWithCount', { count: props?.following.length ?? 0 })}
</div>
</div>
</Match>
<Match when={props.following && props.following.length > 0 && props.isCurrentUser}>
<SharePopup
containerCssClass={stylesHeader.control}
title={props.author.name}
description={props.author.bio}
imageUrl={props.author.userpic}
shareUrl={getShareUrl({ pathname: `/author/${props.author.slug}` })}
trigger={<Button variant="secondary" value={t('Share')} />}
/>
</Match>
</Switch>
</div> </div>
</Show> </Show>
</div> </div>
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={isSessionLoaded()}> <Show when={isSessionLoaded()}>
<div class={styles.authorSubscribeSocial}>
<For each={props.author.links}>
{(link) => (
<a
class={styles.socialLink}
href={link.startsWith('http') ? link : `https://${link}`}
target="_blank"
rel="nofollow noopener"
>
<span class={styles.authorSubscribeSocialLabel}>
{link.startsWith('http') ? link : `https://${link}`}
</span>
</a>
)}
</For>
</div>
<Show when={canFollow()}> <Show when={canFollow()}>
<div class={styles.authorSubscribe}> <div class={styles.authorSubscribe}>
<Show when={!props.noSocialButtons && !props.hideWriteButton && props.author.links}>
<div class={styles.authorSubscribeSocial}>
<For each={props.author.links}>
{(link) => (
<a href={link}>
<span class={styles.authorSubscribeSocialLabel}>{link}</span>
</a>
)}
</For>
</div>
</Show>
<Show <Show
when={subscribed()} when={subscribed()}
fallback={ fallback={

View File

@ -18,6 +18,7 @@ import { useLocalize } from '../../../context/localize'
import { AuthorRatingControl } from '../../Author/AuthorRatingControl' import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { useSession } from '../../../context/session'
type Props = { type Props = {
shouts: Shout[] shouts: Shout[]
@ -34,6 +35,7 @@ export const AuthorView = (props: Props) => {
const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { page } = useRouter() const { page } = useRouter()
const { user } = useSession()
const author = createMemo(() => authorEntities()[props.authorSlug]) const author = createMemo(() => authorEntities()[props.authorSlug])
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
@ -131,6 +133,7 @@ export const AuthorView = (props: Props) => {
isAuthorPage={true} isAuthorPage={true}
followers={followers()} followers={followers()}
following={following()} following={following()}
isCurrentUser={author().slug === user()?.slug}
/> />
</div> </div>
</Show> </Show>
@ -139,13 +142,13 @@ export const AuthorView = (props: Props) => {
<ul class="view-switcher"> <ul class="view-switcher">
<li classList={{ 'view-switcher__item--selected': page().route === 'author' }}> <li classList={{ 'view-switcher__item--selected': page().route === 'author' }}>
<a href={getPagePath(router, 'author', { slug: props.authorSlug })}>{t('Publications')}</a> <a href={getPagePath(router, 'author', { slug: props.authorSlug })}>{t('Publications')}</a>
<span class="view-switcher__counter">{author().stat.shouts}</span> <span class="view-switcher__counter">{author().stat?.shouts}</span>
</li> </li>
<li classList={{ 'view-switcher__item--selected': page().route === 'authorComments' }}> <li classList={{ 'view-switcher__item--selected': page().route === 'authorComments' }}>
<a href={getPagePath(router, 'authorComments', { slug: props.authorSlug })}> <a href={getPagePath(router, 'authorComments', { slug: props.authorSlug })}>
{t('Comments')} {t('Comments')}
</a> </a>
<span class="view-switcher__counter">{author().stat.commented}</span> <span class="view-switcher__counter">{author().stat?.commented}</span>
</li> </li>
<li classList={{ 'view-switcher__item--selected': page().route === 'authorAbout' }}> <li classList={{ 'view-switcher__item--selected': page().route === 'authorAbout' }}>
<a <a

View File

@ -174,14 +174,15 @@ export type Mutation = {
destroyTopic: Result destroyTopic: Result
follow: Result follow: Result
getSession: AuthResult getSession: AuthResult
markAllNotificationsAsRead: Result
markAsRead: Result markAsRead: Result
markNotificationAsRead: Result
rateUser: Result rateUser: Result
registerUser: AuthResult registerUser: AuthResult
sendLink: Result sendLink: Result
unfollow: Result unfollow: Result
updateChat: Result updateChat: Result
updateMessage: Result updateMessage: Result
updateOnlineStatus: Result
updateProfile: Result updateProfile: Result
updateReaction: Result updateReaction: Result
updateShout: Result updateShout: Result
@ -246,6 +247,10 @@ export type MutationMarkAsReadArgs = {
ids: Array<InputMaybe<Scalars['Int']>> ids: Array<InputMaybe<Scalars['Int']>>
} }
export type MutationMarkNotificationAsReadArgs = {
notification_id: Scalars['Int']
}
export type MutationRateUserArgs = { export type MutationRateUserArgs = {
slug: Scalars['String'] slug: Scalars['String']
value: Scalars['Int'] value: Scalars['Int']
@ -297,6 +302,33 @@ export type MutationUpdateTopicArgs = {
input: TopicInput input: TopicInput
} }
export type Notification = {
createdAt: Scalars['DateTime']
data?: Maybe<Scalars['String']>
id: Scalars['Int']
occurrences: Scalars['Int']
reaction?: Maybe<Scalars['Int']>
seen: Scalars['Boolean']
shout?: Maybe<Scalars['Int']>
type?: Maybe<NotificationType>
}
export enum NotificationType {
NewComment = 'NEW_COMMENT',
NewReply = 'NEW_REPLY'
}
export type NotificationsQueryParams = {
limit?: InputMaybe<Scalars['Int']>
offset?: InputMaybe<Scalars['Int']>
}
export type NotificationsQueryResult = {
notifications: Array<Maybe<Notification>>
totalCount: Scalars['Int']
totalUnreadCount: Scalars['Int']
}
export type Operation = { export type Operation = {
id: Scalars['Int'] id: Scalars['Int']
name: Scalars['String'] name: Scalars['String']
@ -325,6 +357,7 @@ export type Query = {
loadChats: Result loadChats: Result
loadDrafts: Array<Maybe<Shout>> loadDrafts: Array<Maybe<Shout>>
loadMessagesBy: Result loadMessagesBy: Result
loadNotifications: NotificationsQueryResult
loadReactionsBy: Array<Maybe<Reaction>> loadReactionsBy: Array<Maybe<Reaction>>
loadRecipients: Result loadRecipients: Result
loadShout?: Maybe<Shout> loadShout?: Maybe<Shout>
@ -373,6 +406,10 @@ export type QueryLoadMessagesByArgs = {
offset?: InputMaybe<Scalars['Int']> offset?: InputMaybe<Scalars['Int']>
} }
export type QueryLoadNotificationsArgs = {
params: NotificationsQueryParams
}
export type QueryLoadReactionsByArgs = { export type QueryLoadReactionsByArgs = {
by: ReactionBy by: ReactionBy
limit?: InputMaybe<Scalars['Int']> limit?: InputMaybe<Scalars['Int']>
@ -599,12 +636,6 @@ export type Stat = {
viewed?: Maybe<Scalars['Int']> viewed?: Maybe<Scalars['Int']>
} }
export type Subscription = {
newMessage?: Maybe<Message>
newReaction?: Maybe<Reaction>
newShout?: Maybe<Shout>
}
export type Token = { export type Token = {
createdAt: Scalars['DateTime'] createdAt: Scalars['DateTime']
expiresAt?: Maybe<Scalars['DateTime']> expiresAt?: Maybe<Scalars['DateTime']>