Feature/refactoring user card (#274)
* Refactoring AuthorCard * fix alphabet sort
This commit is contained in:
parent
891d29ff6a
commit
bfe1ef2e85
|
@ -240,7 +240,6 @@ img {
|
|||
.shoutAuthorsList {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin: 2em 0;
|
||||
padding-bottom: 2em;
|
||||
|
||||
h4 {
|
||||
color: #696969;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
|||
import { router } from '../../stores/router'
|
||||
|
||||
import styles from './Comment.module.scss'
|
||||
import { AuthorLink } from '../Author/AhtorLink'
|
||||
|
||||
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor'))
|
||||
|
||||
|
@ -135,7 +136,6 @@ export const Comment = (props: Props) => {
|
|||
<Userpic
|
||||
name={comment().createdBy.name}
|
||||
userpic={comment().createdBy.userpic}
|
||||
isBig={false}
|
||||
class={clsx({
|
||||
[styles.compactUserpic]: props.compact
|
||||
})}
|
||||
|
@ -148,13 +148,7 @@ export const Comment = (props: Props) => {
|
|||
>
|
||||
<div class={styles.commentDetails}>
|
||||
<div class={styles.commentAuthor}>
|
||||
<AuthorCard
|
||||
author={comment()?.createdBy as Author}
|
||||
hideDescription={true}
|
||||
hideFollow={true}
|
||||
isComments={true}
|
||||
hasLink={true}
|
||||
/>
|
||||
<AuthorLink author={comment()?.createdBy as Author} />
|
||||
</div>
|
||||
|
||||
<Show when={props.isArticleAuthor}>
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
.commentDates {
|
||||
color: #9fa1a7;
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: var(--secondary-color);
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
@include font-size(1.2rem);
|
||||
font-size: 1.2rem;
|
||||
justify-content: flex-start;
|
||||
margin: 0 1em 0 0;
|
||||
margin: 0 1rem;
|
||||
height: 1.6rem;
|
||||
|
||||
.date {
|
||||
font-weight: 500;
|
||||
|
|
|
@ -26,6 +26,7 @@ import { SolidSwiper } from '../_shared/SolidSwiper'
|
|||
import styles from './Article.module.scss'
|
||||
import { CardTopic } from '../Feed/CardTopic'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
|
||||
type Props = {
|
||||
article: Shout
|
||||
|
@ -437,9 +438,9 @@ export const FullArticle = (props: Props) => {
|
|||
<h4>{t('Authors')}</h4>
|
||||
</Show>
|
||||
<For each={props.article.authors}>
|
||||
{(a) => (
|
||||
{(author) => (
|
||||
<div class="col-xl-12">
|
||||
<AuthorCard author={a} hasLink={true} liteButtons={true} />
|
||||
<AuthorBadge iconButtons={true} showMessageButton={true} author={author} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
49
src/components/Author/AhtorLink/AhtorLink.module.scss
Normal file
49
src/components/Author/AhtorLink/AhtorLink.module.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
.AuthorLink {
|
||||
.link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
|
||||
&:hover {
|
||||
background: unset !important;
|
||||
border-bottom: none;
|
||||
color: var(--default-color) !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
margin-bottom: -2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--default-color-invert);
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// adjust size
|
||||
&.XS {
|
||||
.link {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.M {
|
||||
.link {
|
||||
gap: 1rem;
|
||||
}
|
||||
.name {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
21
src/components/Author/AhtorLink/AuthorLink.tsx
Normal file
21
src/components/Author/AhtorLink/AuthorLink.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AhtorLink.module.scss'
|
||||
import { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
size?: 'XS' | 'M' | 'L'
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const AuthorLink = (props: Props) => {
|
||||
return (
|
||||
<div class={clsx(styles.AuthorLink, props.class, styles[props.size ?? 'M'])}>
|
||||
<a class={styles.link} href={`/author/${props.author.slug}`}>
|
||||
<Userpic size={props.size ?? 'M'} name={props.author.name} userpic={props.author.userpic} />
|
||||
<div class={styles.name}>{props.author.name}</div>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Author/AhtorLink/index.ts
Normal file
1
src/components/Author/AhtorLink/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AuthorLink } from './AuthorLink'
|
|
@ -3,6 +3,7 @@
|
|||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex-wrap: wrap;
|
||||
|
@ -33,6 +34,11 @@
|
|||
.name {
|
||||
color: var(--default-color);
|
||||
font-weight: 500;
|
||||
|
||||
& span:hover {
|
||||
color: var(--default-color-invert);
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
|
||||
.bio {
|
||||
|
@ -42,7 +48,10 @@
|
|||
|
||||
.actions {
|
||||
flex: 0 20%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 5.2rem;
|
||||
gap: 1rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 2rem;
|
||||
|
@ -56,9 +65,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.subscribeButton {
|
||||
.actionButton {
|
||||
border-radius: 0.8rem !important;
|
||||
margin-right: 0 !important;
|
||||
width: 9em;
|
||||
|
||||
&.iconed {
|
||||
padding: 6px !important;
|
||||
min-width: 32px;
|
||||
width: unset;
|
||||
&:hover img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,10 +8,15 @@ import { Button } from '../../_shared/Button'
|
|||
import { useSession } from '../../../context/session'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
minimizeSubscribeButton?: boolean
|
||||
showMessageButton?: boolean
|
||||
iconButtons?: boolean
|
||||
}
|
||||
export const AuthorBadge = (props: Props) => {
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
|
@ -20,7 +25,7 @@ export const AuthorBadge = (props: Props) => {
|
|||
subscriptions,
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
} = useSession()
|
||||
|
||||
const { changeSearchParam } = useRouter()
|
||||
const { t, formatDate } = useLocalize()
|
||||
const subscribed = createMemo(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
|
@ -42,17 +47,34 @@ export const AuthorBadge = (props: Props) => {
|
|||
}, 'subscribe')
|
||||
}
|
||||
|
||||
const initChat = () => {
|
||||
requireAuthentication(() => {
|
||||
openPage(router, `inbox`)
|
||||
changeSearchParam({
|
||||
initChat: props.author.id.toString()
|
||||
})
|
||||
}, 'discussions')
|
||||
}
|
||||
const subscribeValue = createMemo(() => {
|
||||
if (props.iconButtons) {
|
||||
return <Icon name="author-subscribe" />
|
||||
}
|
||||
return isSubscribing() ? t('...subscribing') : t('Subscribe')
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.AuthorBadge)}>
|
||||
<Userpic
|
||||
hasLink={true}
|
||||
isMedium={true}
|
||||
size={'M'}
|
||||
name={props.author.name}
|
||||
userpic={props.author.userpic}
|
||||
slug={props.author.slug}
|
||||
/>
|
||||
<a href={`/author/${props.author.slug}`} class={styles.info}>
|
||||
<div class={styles.name}>{props.author.name}</div>
|
||||
<div class={styles.name}>
|
||||
<span>{props.author.name}</span>
|
||||
</div>
|
||||
<Switch
|
||||
fallback={
|
||||
<div class={styles.bio}>
|
||||
|
@ -86,23 +108,32 @@ export const AuthorBadge = (props: Props) => {
|
|||
when={subscribed()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="primary"
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="S"
|
||||
value={isSubscribing() ? t('...subscribing') : t('Subscribe')}
|
||||
value={subscribeValue()}
|
||||
onClick={() => handleSubscribe(true)}
|
||||
class={styles.subscribeButton}
|
||||
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="bordered"
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="S"
|
||||
value={t('Following')}
|
||||
value={props.iconButtons ? <Icon name="author-unsubscribe" /> : t('Following')}
|
||||
onClick={() => handleSubscribe(false)}
|
||||
class={styles.subscribeButton}
|
||||
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}
|
||||
/>
|
||||
</Show>
|
||||
</Show>
|
||||
<Show when={props.showMessageButton}>
|
||||
<Button
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="S"
|
||||
value={props.iconButtons ? <Icon name="inbox-white" /> : t('Message')}
|
||||
onClick={initChat}
|
||||
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
flex-flow: row nowrap;
|
||||
margin-bottom: 1.6rem;
|
||||
|
||||
|
@ -8,10 +8,47 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
@include font-size(4rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
color: #696969;
|
||||
@include font-size(2rem);
|
||||
font-weight: 500;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.authorActions {
|
||||
margin: 2rem -0.8rem 0 0;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex: 1 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.listWrapper & {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
|
@ -39,7 +76,7 @@
|
|||
}
|
||||
|
||||
.authorDetails {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
align-items: center;
|
||||
|
@ -57,10 +94,6 @@
|
|||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.authorDetailsShrinked {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetailsWrapper {
|
||||
|
@ -84,29 +117,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.authorNameContainer {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
border: none !important;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
.listWrapper & {
|
||||
display: block;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
@include font-size(4rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
|
@ -117,42 +130,6 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.authorSubscribe {
|
||||
align-items: center;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
@ -426,208 +403,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.shareControl {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.buttonSubscribe {
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 100%;
|
||||
display: inline-flex;
|
||||
float: right;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.buttonLabelVisible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttonWrite {
|
||||
background: #ccc;
|
||||
color: #000;
|
||||
display: inline-flex;
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
|
||||
img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 15px;
|
||||
transition: filter 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.authorPage {
|
||||
align-items: center;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
@include font-size(4rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
color: #696969;
|
||||
@include font-size(2rem);
|
||||
font-weight: 500;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.authorSubscribe {
|
||||
margin: 2rem -0.8rem 0 0;
|
||||
padding-left: 0;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
flex: 1 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLabel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttonSubscribe {
|
||||
aspect-ratio: auto;
|
||||
background-color: #000;
|
||||
border-color: #000;
|
||||
border-radius: 0.8rem;
|
||||
color: #fff;
|
||||
float: none;
|
||||
padding-bottom: 0.6rem;
|
||||
padding-top: 0.6rem;
|
||||
width: 10em;
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
|
||||
img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
|
||||
.icon img {
|
||||
filter: invert(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonSubscribe img {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-height: 4rem;
|
||||
margin: 0 0.8rem 0 0;
|
||||
vertical-align: middle;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.authorsListItem {
|
||||
margin-bottom: 1em !important;
|
||||
|
||||
.authorName {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.authorSubscribe {
|
||||
align-items: baseline;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonLabel {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.nowrapView {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.authorComments {
|
||||
.authorName {
|
||||
@include font-size(1.2rem);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.circlewrap {
|
||||
margin-top: -0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.isSubscribing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.feedMode {
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
|
||||
.authorName,
|
||||
.authorAbout {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.circlewrap {
|
||||
height: 1.6rem;
|
||||
margin-right: 0.4rem;
|
||||
min-width: 1.6rem;
|
||||
width: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -648,33 +423,33 @@
|
|||
vertical-align: top;
|
||||
border-bottom: unset !important;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
.subscribersItem {
|
||||
position: relative;
|
||||
|
||||
&:nth-child(1) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.userpic {
|
||||
background: var(--background-color);
|
||||
box-shadow: 0 0 0 2px var(--background-color);
|
||||
height: 1.8rem;
|
||||
min-width: 1.8rem;
|
||||
max-width: 1.8rem;
|
||||
vertical-align: top;
|
||||
width: 1.8rem;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: -1.8rem;
|
||||
&:nth-child(2) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> * {
|
||||
line-height: 1.8rem;
|
||||
min-width: auto;
|
||||
&:not(:last-child) {
|
||||
margin-right: -4px;
|
||||
box-shadow: 0 0 0 1px var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersCounter {
|
||||
.subscribersCounter {
|
||||
font-weight: 500;
|
||||
margin-left: -0.6rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
|
||||
.subscribersCounter {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
|
@ -11,7 +10,6 @@ import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
|||
import { router, useRouter } from '../../../stores/router'
|
||||
import { openPage, redirectPage } from '@nanostores/router'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
|
@ -22,28 +20,9 @@ import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
|||
import styles from './AuthorCard.module.scss'
|
||||
|
||||
type Props = {
|
||||
caption?: string
|
||||
hideWriteButton?: boolean
|
||||
hideDescription?: boolean
|
||||
hideFollow?: boolean
|
||||
hasLink?: boolean
|
||||
subscribed?: boolean
|
||||
author: Author
|
||||
isAuthorPage?: boolean
|
||||
noSocialButtons?: boolean
|
||||
isAuthorsList?: boolean
|
||||
truncateBio?: boolean
|
||||
liteButtons?: boolean
|
||||
isTextButton?: boolean
|
||||
isComments?: boolean
|
||||
isFeedMode?: boolean
|
||||
isNowrap?: boolean
|
||||
class?: string
|
||||
followers?: Author[]
|
||||
following?: Array<Author | Topic>
|
||||
showPublicationsCounter?: boolean
|
||||
hideBio?: boolean
|
||||
isCurrentUser?: boolean
|
||||
}
|
||||
|
||||
export const AuthorCard = (props: Props) => {
|
||||
|
@ -58,7 +37,6 @@ export const AuthorCard = (props: Props) => {
|
|||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following)
|
||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
const [userpicUrl, setUserpicUrl] = createSignal<string>()
|
||||
|
||||
const subscribed = createMemo<boolean>(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
|
@ -75,7 +53,7 @@ export const AuthorCard = (props: Props) => {
|
|||
setIsSubscribing(false)
|
||||
}
|
||||
|
||||
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
||||
const isProfileOwner = createMemo(() => session()?.user?.slug === props.author.slug)
|
||||
|
||||
const name = createMemo(() => {
|
||||
if (lang() !== 'ru') {
|
||||
|
@ -102,7 +80,7 @@ export const AuthorCard = (props: Props) => {
|
|||
|
||||
const handleSubscribe = () => {
|
||||
requireAuthentication(() => {
|
||||
subscribe(true)
|
||||
subscribe(!subscribed())
|
||||
}, 'subscribe')
|
||||
}
|
||||
|
||||
|
@ -118,89 +96,28 @@ export const AuthorCard = (props: Props) => {
|
|||
}
|
||||
})
|
||||
|
||||
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) {
|
||||
setUserpicUrl(props.author.userpic.replace('100x', '500x500'))
|
||||
const followButtonText = () => {
|
||||
if (isSubscribing()) {
|
||||
return t('...subscribing')
|
||||
}
|
||||
return t(subscribed() ? 'Unfollow' : 'Follow')
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.author, props.class)}
|
||||
classList={{
|
||||
['row']: props.isAuthorPage,
|
||||
[styles.authorPage]: props.isAuthorPage,
|
||||
[styles.authorComments]: props.isComments,
|
||||
[styles.authorsListItem]: props.isAuthorsList,
|
||||
[styles.feedMode]: props.isFeedMode,
|
||||
[styles.nowrapView]: props.isNowrap
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={props.isAuthorPage}
|
||||
fallback={
|
||||
<Userpic
|
||||
name={props.author.name}
|
||||
userpic={props.author.userpic}
|
||||
hasLink={props.hasLink}
|
||||
isBig={props.isAuthorPage}
|
||||
isAuthorsList={props.isAuthorsList}
|
||||
isFeedMode={props.isFeedMode}
|
||||
slug={props.author.slug}
|
||||
class={styles.circlewrap}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div class={clsx(styles.author, 'row')}>
|
||||
<div class="col-md-5">
|
||||
<Userpic
|
||||
size={'XL'}
|
||||
name={props.author.name}
|
||||
userpic={userpicUrl()}
|
||||
hasLink={props.hasLink}
|
||||
isBig={props.isAuthorPage}
|
||||
isAuthorsList={props.isAuthorsList}
|
||||
isFeedMode={props.isFeedMode}
|
||||
userpic={props.author.userpic}
|
||||
slug={props.author.slug}
|
||||
class={styles.circlewrap}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
class={styles.authorDetails}
|
||||
classList={{
|
||||
'col-md-15 col-xl-13': props.isAuthorPage,
|
||||
[styles.authorDetailsShrinked]: props.isAuthorPage
|
||||
}}
|
||||
>
|
||||
<div class={clsx('col-md-15 col-xl-13', styles.authorDetails)}>
|
||||
<div class={styles.authorDetailsWrapper}>
|
||||
<div class={styles.authorNameContainer}>
|
||||
<ConditionalWrapper
|
||||
condition={props.hasLink}
|
||||
wrapper={(children) => (
|
||||
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
|
||||
{children}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
<Show
|
||||
when={props.author.bio && !props.hideBio}
|
||||
fallback={
|
||||
props.showPublicationsCounter ? (
|
||||
<div class={styles.authorAbout}>
|
||||
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={styles.authorAbout}
|
||||
classList={{ 'text-truncate': props.truncateBio }}
|
||||
innerHTML={props.author.bio}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<div class={styles.authorName}>{name()}</div>
|
||||
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
||||
<Show
|
||||
when={
|
||||
(props.followers && props.followers.length > 0) ||
|
||||
|
@ -211,10 +128,17 @@ export const AuthorCard = (props: Props) => {
|
|||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<a href="?modal=followers" class={styles.subscribers}>
|
||||
<For each={props.followers.slice(0, 3)}>
|
||||
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />}
|
||||
{(f) => (
|
||||
<Userpic
|
||||
size={'XS'}
|
||||
name={f.name}
|
||||
userpic={f.userpic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriberWithCount', { count: props.followers.length })}
|
||||
{t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
|
||||
</div>
|
||||
</a>
|
||||
</Show>
|
||||
|
@ -224,9 +148,23 @@ export const AuthorCard = (props: Props) => {
|
|||
<For each={props.following.slice(0, 3)}>
|
||||
{(f) => {
|
||||
if ('name' in f) {
|
||||
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />
|
||||
return (
|
||||
<Userpic
|
||||
size={'XS'}
|
||||
name={f.name}
|
||||
userpic={f.userpic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)
|
||||
} else if ('title' in f) {
|
||||
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} />
|
||||
return (
|
||||
<Userpic
|
||||
size={'XS'}
|
||||
name={f.title}
|
||||
userpic={f.pic}
|
||||
class={styles.subscribersItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}}
|
||||
|
@ -259,102 +197,23 @@ export const AuthorCard = (props: Props) => {
|
|||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={canFollow()}>
|
||||
<div class={styles.authorSubscribe}>
|
||||
|
||||
<Show
|
||||
when={subscribed()}
|
||||
when={isProfileOwner()}
|
||||
fallback={
|
||||
<button
|
||||
onClick={handleSubscribe}
|
||||
class={clsx('button', styles.button)}
|
||||
classList={{
|
||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||
'button--subscribe': !props.isAuthorsList,
|
||||
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
||||
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
||||
[styles.isSubscribing]: isSubscribing()
|
||||
}}
|
||||
disabled={isSubscribing()}
|
||||
>
|
||||
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||
<Icon name="author-subscribe" class={styles.icon} />
|
||||
</Show>
|
||||
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
|
||||
{t('Follow')}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
<div class={styles.authorActions}>
|
||||
<Button onClick={handleSubscribe} value={followButtonText()} />
|
||||
<Button variant={'secondary'} value={t('Message')} onClick={initChat} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => subscribe(false)}
|
||||
class={clsx('button', styles.button)}
|
||||
classList={{
|
||||
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
|
||||
'button--subscribe': !props.isAuthorsList,
|
||||
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
|
||||
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
|
||||
[styles.isSubscribing]: isSubscribing()
|
||||
}}
|
||||
disabled={isSubscribing()}
|
||||
>
|
||||
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
|
||||
<Icon name="author-unsubscribe" class={styles.icon} />
|
||||
</Show>
|
||||
<Show when={props.isTextButton || props.isAuthorPage}>
|
||||
<span
|
||||
class={clsx(
|
||||
styles.buttonLabel,
|
||||
styles.buttonLabelVisible,
|
||||
styles.buttonUnfollowLabel
|
||||
)}
|
||||
>
|
||||
{t('Unfollow')}
|
||||
</span>
|
||||
<span
|
||||
class={clsx(
|
||||
styles.buttonLabel,
|
||||
styles.buttonLabelVisible,
|
||||
styles.buttonSubscribedLabel
|
||||
)}
|
||||
>
|
||||
{t('Following')}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
<Show when={!props.hideWriteButton}>
|
||||
<button
|
||||
class={styles.button}
|
||||
classList={{
|
||||
'button--light': !props.isAuthorsList,
|
||||
'button--subscribe-topic': props.isAuthorsList,
|
||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||
}}
|
||||
onClick={initChat}
|
||||
>
|
||||
<Show when={!props.isTextButton && !props.isAuthorPage}>
|
||||
<Icon name="comment" class={styles.icon} />
|
||||
</Show>
|
||||
<Show when={!props.liteButtons || props.isTextButton}>{t('Message')}</Show>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.isCurrentUser}>
|
||||
<div class={styles.authorSubscribe}>
|
||||
<div class={styles.authorActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => redirectPage(router, 'profileSettings')}
|
||||
value={t('Edit profile')}
|
||||
class={styles.button}
|
||||
/>
|
||||
|
||||
<SharePopup
|
||||
containerCssClass={styles.shareControl}
|
||||
title={props.author.name}
|
||||
description={props.author.bio}
|
||||
imageUrl={props.author.userpic}
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
.Userpic {
|
||||
align-items: baseline;
|
||||
background: #f7f7f8;
|
||||
background: #f7f7f7;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 1.2rem;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
|
@ -20,17 +15,17 @@
|
|||
}
|
||||
|
||||
.letters {
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid black;
|
||||
color: #000;
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
line-height: 32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 32px;
|
||||
width: 100%;
|
||||
border-radius: 100%;
|
||||
padding-top: 2px; // line-height hack
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--default-color);
|
||||
text-transform: uppercase;
|
||||
background: var(--background-color);
|
||||
box-shadow: 0 0 0 1px var(--background-color-invert) inset;
|
||||
}
|
||||
|
||||
.anonymous {
|
||||
|
@ -55,18 +50,39 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
&.XS {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.letters {
|
||||
line-height: 40px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.big {
|
||||
&.S {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
overflow: hidden;
|
||||
|
||||
.letters {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.M {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
|
||||
.letters {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.XL {
|
||||
aspect-ratio: 1/1;
|
||||
margin: 0 auto 1rem;
|
||||
max-width: 168px;
|
||||
|
@ -101,12 +117,3 @@
|
|||
line-height: 6.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.feedMode {
|
||||
.letters {
|
||||
font-size: 0.8rem;
|
||||
line-height: 14px;
|
||||
min-width: 16px;
|
||||
max-width: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Show } from 'solid-js'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import styles from './Userpic.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
|
@ -12,27 +12,47 @@ type Props = {
|
|||
slug?: string
|
||||
onClick?: () => void
|
||||
loading?: boolean
|
||||
isBig?: boolean
|
||||
isMedium?: boolean
|
||||
hasLink?: boolean
|
||||
isAuthorsList?: boolean
|
||||
isFeedMode?: boolean
|
||||
size?: 'XS' | 'S' | 'M' | 'L' | 'XL' // 20 | 28 | 32 | 40 | 168
|
||||
}
|
||||
|
||||
export const Userpic = (props: Props) => {
|
||||
const [userpicUrl, setUserpicUrl] = createSignal<string>()
|
||||
const letters = () => {
|
||||
if (!props.name) return
|
||||
const names = props.name ? props.name.split(' ') : []
|
||||
return names[0][0] + (names.length > 1 ? names[1][0] : '')
|
||||
}
|
||||
|
||||
const comutedAvatarSize = () => {
|
||||
switch (props.size) {
|
||||
case 'XS': {
|
||||
return '40x40'
|
||||
}
|
||||
case 'S': {
|
||||
return '56x56'
|
||||
}
|
||||
case 'L': {
|
||||
return '80x80'
|
||||
}
|
||||
case 'XL': {
|
||||
return '336x336'
|
||||
}
|
||||
default: {
|
||||
return '64x64'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setUserpicUrl(
|
||||
props.userpic && props.userpic.includes('100x')
|
||||
? props.userpic.replace('100x', comutedAvatarSize())
|
||||
: props.userpic
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.Userpic, props.class, {
|
||||
[styles.big]: props.isBig,
|
||||
[styles.medium]: props.isMedium,
|
||||
[styles.authorsList]: props.isAuthorsList,
|
||||
[styles.feedMode]: props.isFeedMode,
|
||||
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
||||
['cursorPointer']: props.onClick
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
|
@ -47,7 +67,7 @@ export const Userpic = (props: Props) => {
|
|||
fallback={
|
||||
<img
|
||||
class={clsx({ [styles.anonymous]: !props.userpic })}
|
||||
src={imageProxy(props.userpic) || '/icons/user-default.svg'}
|
||||
src={imageProxy(userpicUrl()) || '/icons/user-default.svg'}
|
||||
alt={props.name || ''}
|
||||
loading="lazy"
|
||||
/>
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
|
||||
a:link {
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
|
@ -1,22 +1,22 @@
|
|||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import type { Shout } from '../../../graphql/types.gen'
|
||||
import { capitalize } from '../../../utils/capitalize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import styles from './ArticleCard.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { CardTopic } from './CardTopic'
|
||||
import { ShoutRatingControl } from '../Article/ShoutRatingControl'
|
||||
import { getShareUrl, SharePopup } from '../Article/SharePopup'
|
||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
import { FeedArticlePopup } from './FeedArticlePopup'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { CardTopic } from '../CardTopic'
|
||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { FeedArticlePopup } from '../FeedArticlePopup'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../stores/router'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { useSession } from '../../context/session'
|
||||
import { capitalize } from '../../utils/capitalize'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { AuthorLink } from '../../Author/AhtorLink'
|
||||
|
||||
interface ArticleCardProps {
|
||||
settings?: {
|
||||
|
@ -180,17 +180,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
<div class={styles.shoutAuthor}>
|
||||
<For each={props.article.authors}>
|
||||
{(author) => {
|
||||
return (
|
||||
<AuthorCard
|
||||
author={author}
|
||||
hideWriteButton={true}
|
||||
hideBio={true}
|
||||
hideFollow={true}
|
||||
truncateBio={true}
|
||||
isFeedMode={true}
|
||||
hasLink={!props.settings?.noAuthorLink}
|
||||
/>
|
||||
)
|
||||
return <AuthorLink size={'XS'} author={author} />
|
||||
}}
|
||||
</For>
|
||||
</div>
|
1
src/components/Feed/ArticleCard/index.tsx
Normal file
1
src/components/Feed/ArticleCard/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { ArticleCard } from './ArticleCard'
|
|
@ -201,6 +201,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<button class={styles.button}>
|
||||
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||
<Userpic
|
||||
size={'M'}
|
||||
name={session().user.name}
|
||||
userpic={session().user.userpic}
|
||||
class={styles.userpic}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
|
||||
import { translit } from '../../utils/ru2en'
|
||||
import { hideModal } from '../../stores/ui'
|
||||
import { createMemo, For } from 'solid-js'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
||||
export const ProfileModal = () => {
|
||||
const {
|
||||
session,
|
||||
actions: { signOut }
|
||||
} = useSession()
|
||||
|
||||
const quit = () => {
|
||||
signOut()
|
||||
hideModal()
|
||||
}
|
||||
const { t, lang } = useLocalize()
|
||||
|
||||
const author = createMemo<Author>(() => {
|
||||
const a: Author = {
|
||||
id: null,
|
||||
name: 'anonymous',
|
||||
userpic: '',
|
||||
slug: ''
|
||||
}
|
||||
|
||||
if (session()?.user?.slug) {
|
||||
const u = session().user
|
||||
a.name = lang() === 'ru' ? u.name : translit(u.name)
|
||||
a.slug = u.slug
|
||||
a.userpic = u.userpic
|
||||
}
|
||||
|
||||
return a
|
||||
})
|
||||
|
||||
// TODO: ProfileModal markup and styles
|
||||
return (
|
||||
<div class="row view profile">
|
||||
<h1>{session()?.user?.username}</h1>
|
||||
<AuthorCard author={author()} />
|
||||
<div class="profile-bio">{session()?.user?.bio || ''}</div>
|
||||
<For each={session()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For>
|
||||
<span onClick={quit}>{t('Quit')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -30,7 +30,8 @@
|
|||
}
|
||||
|
||||
.userpic {
|
||||
margin-right: 15px;
|
||||
min-width: 40px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.timeContainer {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { clsx } from 'clsx'
|
||||
import type { Notification } from '../../../graphql/types.gen'
|
||||
import type { Author, Notification } from '../../../graphql/types.gen'
|
||||
import { createMemo, createSignal, onMount, Show } from 'solid-js'
|
||||
import { NotificationType } from '../../../graphql/types.gen'
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { useNotifications } from '../../../context/notifications'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import type { ArticlePageSearchParams } from '../../Article/FullArticle'
|
||||
import { TimeAgo } from '../../_shared/TimeAgo'
|
||||
import styles from './NotificationView.module.scss'
|
||||
import { GroupAvatar } from '../../_shared/GroupAvatar'
|
||||
|
||||
type Props = {
|
||||
notification: Notification
|
||||
|
@ -18,17 +18,19 @@ type Props = {
|
|||
class?: string
|
||||
}
|
||||
|
||||
export type NotificationUser = {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
userpic: string
|
||||
}
|
||||
|
||||
type NotificationData = {
|
||||
shout: {
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
users: {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
userpic: string
|
||||
}[]
|
||||
users: NotificationUser[]
|
||||
reactionIds: number[]
|
||||
}
|
||||
|
||||
|
@ -160,7 +162,9 @@ export const NotificationView = (props: Props) => {
|
|||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Userpic name={lastUser().name} userpic={lastUser().userpic} class={styles.userpic} />
|
||||
<div class={styles.userpic}>
|
||||
<GroupAvatar authors={data().users} />
|
||||
</div>
|
||||
<div>{content()}</div>
|
||||
<div class={styles.timeContainer}>{formattedDateTime()}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
|
||||
import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { SearchField } from '../_shared/SearchField'
|
||||
import { scrollHandler } from '../../utils/scroll'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { dummyFilter } from '../../utils/dummyFilter'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
|
||||
import styles from './AllAuthors.module.scss'
|
||||
|
||||
|
@ -35,8 +33,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
|
||||
const [searchQuery, setSearchQuery] = createSignal('')
|
||||
|
||||
const { session, subscriptions } = useSession()
|
||||
|
||||
onMount(() => {
|
||||
if (!searchParams().by) {
|
||||
changeSearchParam({
|
||||
|
@ -50,9 +46,18 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
})
|
||||
|
||||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||
return sortedAuthors().reduce(
|
||||
return sortedAuthors()
|
||||
.slice(0, 1)
|
||||
.reduce(
|
||||
(acc, author) => {
|
||||
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
|
||||
let letter = ''
|
||||
if (author && author.name) {
|
||||
const nameParts = author.name.trim().split(' ')
|
||||
const lastName = nameParts.pop()
|
||||
if (lastName && lastName.length > 0) {
|
||||
letter = lastName[0].toUpperCase()
|
||||
}
|
||||
}
|
||||
|
||||
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@'
|
||||
|
||||
|
@ -72,9 +77,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
return keys
|
||||
})
|
||||
|
||||
const subscribed = (authorSlug: string) =>
|
||||
subscriptions().authors.some((author) => author.slug === authorSlug)
|
||||
|
||||
const filteredAuthors = createMemo(() => {
|
||||
return dummyFilter(sortedAuthors(), searchQuery(), lang())
|
||||
})
|
||||
|
@ -85,7 +87,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
<div class="col-lg-20 col-xl-18">
|
||||
<h1>{t('Authors')}</h1>
|
||||
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
||||
|
||||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||||
<li
|
||||
classList={{
|
||||
|
@ -173,15 +174,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
{(author) => (
|
||||
<div class="row">
|
||||
<div class="col-lg-20 col-xl-18">
|
||||
<AuthorCard
|
||||
author={author as Author}
|
||||
hasLink={true}
|
||||
subscribed={subscribed(author.slug)}
|
||||
noSocialButtons={true}
|
||||
isAuthorsList={true}
|
||||
truncateBio={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
<AuthorBadge author={author as Author} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -128,13 +128,7 @@ export const AuthorView = (props: Props) => {
|
|||
<Show when={author()} fallback={<Loading />}>
|
||||
<>
|
||||
<div class={styles.authorHeader}>
|
||||
<AuthorCard
|
||||
author={author()}
|
||||
isAuthorPage={true}
|
||||
followers={followers()}
|
||||
following={following()}
|
||||
isCurrentUser={author().slug === user()?.slug}
|
||||
/>
|
||||
<AuthorCard author={author()} followers={followers()} following={following()} />
|
||||
</div>
|
||||
<div class={clsx(styles.groupControls, 'row')}>
|
||||
<div class="col-md-16">
|
||||
|
|
|
@ -156,6 +156,7 @@
|
|||
.commentDetails {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
@ -169,6 +170,9 @@
|
|||
a {
|
||||
border: none;
|
||||
padding-bottom: 0.2em;
|
||||
&:hover * {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ import stylesTopic from '../Feed/CardTopic.module.scss'
|
|||
import stylesBeside from '../../components/Feed/Beside.module.scss'
|
||||
import { CommentDate } from '../Article/CommentDate'
|
||||
import { Loading } from '../_shared/Loading'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { AuthorLink } from '../Author/AhtorLink'
|
||||
|
||||
export const FEED_PAGE_SIZE = 20
|
||||
|
||||
|
@ -163,13 +165,7 @@ export const FeedView = (props: Props) => {
|
|||
<For each={topAuthors().slice(0, 5)}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<AuthorCard
|
||||
author={author}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
truncateBio={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
<AuthorBadge author={author} />
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
|
@ -207,13 +203,7 @@ export const FeedView = (props: Props) => {
|
|||
/>
|
||||
</div>
|
||||
<div class={styles.commentDetails}>
|
||||
<AuthorCard
|
||||
author={comment.createdBy as Author}
|
||||
isFeedMode={true}
|
||||
hideWriteButton={true}
|
||||
hideFollow={true}
|
||||
hasLink={true}
|
||||
/>
|
||||
<AuthorLink author={comment.createdBy as Author} size={'XS'} />
|
||||
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
||||
</div>
|
||||
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
||||
|
|
88
src/components/_shared/GroupAvatar/GroupAvatar.module.scss
Normal file
88
src/components/_shared/GroupAvatar/GroupAvatar.module.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
.GroupAvatar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&.two {
|
||||
.item {
|
||||
border: 1px solid var(--background-color);
|
||||
|
||||
&:nth-child(1) {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.three {
|
||||
.item {
|
||||
&:nth-child(1) {
|
||||
left: 14px;
|
||||
top: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
left: 0;
|
||||
top: 17px;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
right: 0;
|
||||
top: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.four {
|
||||
.item {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
min-width: 19px;
|
||||
|
||||
&:nth-child(1) {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moreUsers {
|
||||
@include font-size(0.75rem);
|
||||
|
||||
font-weight: 700;
|
||||
position: absolute;
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
overflow: hidden;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--black-100);
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
43
src/components/_shared/GroupAvatar/GroupAvatar.tsx
Normal file
43
src/components/_shared/GroupAvatar/GroupAvatar.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './GroupAvatar.module.scss'
|
||||
import { For } from 'solid-js'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { NotificationUser } from '../../NotificationsPanel/NotificationView/NotificationView'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
authors: NotificationUser[]
|
||||
}
|
||||
|
||||
export const GroupAvatar = (props: Props) => {
|
||||
const displayedAvatars = props.authors.length > 4 ? props.authors.slice(0, 3) : props.authors.slice(0, 4)
|
||||
const avatarSize = () => {
|
||||
switch (props.authors.length) {
|
||||
case 1: {
|
||||
return 'L'
|
||||
}
|
||||
case 2: {
|
||||
return 'S'
|
||||
}
|
||||
default: {
|
||||
return 'XS'
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.GroupAvatar, props.class, {
|
||||
[styles.two]: props.authors.length === 2,
|
||||
[styles.three]: props.authors.length === 3,
|
||||
[styles.four]: props.authors.length >= 4
|
||||
})}
|
||||
>
|
||||
<For each={displayedAvatars}>
|
||||
{(user) => (
|
||||
<Userpic size={avatarSize()} name={user.name} userpic={user.userpic} class={styles.item} />
|
||||
)}
|
||||
</For>
|
||||
{props.authors.length > 4 && <div class={styles.moreUsers}>+{props.authors?.length - 3}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/_shared/GroupAvatar/index.ts
Normal file
1
src/components/_shared/GroupAvatar/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { GroupAvatar } from './GroupAvatar'
|
|
@ -22,12 +22,7 @@ export const VotersList = (props: Props) => {
|
|||
{(reaction) => (
|
||||
<li class={styles.item}>
|
||||
<div class={styles.user}>
|
||||
<Userpic
|
||||
name={reaction.createdBy.name}
|
||||
userpic={reaction.createdBy.userpic}
|
||||
isBig={false}
|
||||
isAuthorsList={false}
|
||||
/>
|
||||
<Userpic name={reaction.createdBy.name} userpic={reaction.createdBy.userpic} />
|
||||
<a href={`/author/${reaction.createdBy.slug}`}>{reaction.createdBy.name || ''}</a>
|
||||
</div>
|
||||
{reaction.kind === ReactionKind.Like ? (
|
||||
|
|
|
@ -127,7 +127,7 @@ export const ProfileSettingsPage = () => {
|
|||
<Userpic
|
||||
name={form.name}
|
||||
userpic={form.userpic}
|
||||
isBig={true}
|
||||
size={'XL'}
|
||||
onClick={handleAvatarClick}
|
||||
loading={isUserpicUpdating()}
|
||||
/>
|
||||
|
|
Loading…
Reference in New Issue
Block a user