Feature/subscribe settings (#224)

Subscribe settings page
This commit is contained in:
Ilya Y 2023-09-18 19:33:22 +03:00 committed by GitHub
parent 141ab3d0ed
commit 3d6c6ccbc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 383 additions and 258 deletions

View File

@ -0,0 +1,3 @@
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4.5L4.66667 8L12 1" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.99993 6.06066L8.71224 9.77297L9.7729 8.71231L6.06059 5L9.7729 1.28769L8.71224 0.227029L4.99993 3.93934L1.28762 0.227029L0.226959 1.28769L3.93927 5L0.226959 8.71231L1.28762 9.77297L4.99993 6.06066Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -9,9 +9,11 @@
"Add audio": "Add audio", "Add audio": "Add audio",
"Add blockquote": "Add blockquote", "Add blockquote": "Add blockquote",
"Add comment": "Comment", "Add comment": "Comment",
"Here you can manage all your Discourse subscriptions": "Here you can manage all your Discourse subscriptions",
"Add cover": "Add cover", "Add cover": "Add cover",
"Add image": "Add image", "Add image": "Add image",
"Add images": "Add images", "Add images": "Add images",
"Collections": "Collections",
"Add intro": "Add intro", "Add intro": "Add intro",
"Add link": "Add link", "Add link": "Add link",
"Add rule": "Add rule", "Add rule": "Add rule",

View File

@ -75,6 +75,7 @@
"Confirm": "Подтвердить", "Confirm": "Подтвердить",
"Cooperate": "Соучаствовать", "Cooperate": "Соучаствовать",
"Copy": "Скопировать", "Copy": "Скопировать",
"Collections": "Коллекции",
"Copy link": "Скопировать ссылку", "Copy link": "Скопировать ссылку",
"Corrections history": "История правок", "Corrections history": "История правок",
"Create Chat": "Создать чат", "Create Chat": "Создать чат",
@ -208,6 +209,7 @@
"Move up": "Переместить вверх", "Move up": "Переместить вверх",
"My feed": "Моя лента", "My feed": "Моя лента",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Here you can manage all your Discourse subscriptions": "Здесь можно управлять всеми своими подписками на Дискурсе",
"Name": "Имя", "Name": "Имя",
"Newsletter": "Рассылка", "Newsletter": "Рассылка",
"New literary work": "Новое произведение", "New literary work": "Новое произведение",

View File

@ -17,8 +17,10 @@ import { Modal } from '../../Nav/Modal'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'
import { TopicCard } from '../../Topic/Card' import { TopicCard } from '../../Topic/Card'
import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension' import { getNumeralsDeclension } from '../../../utils/getNumeralsDeclension'
import { SubscriptionFilter } from '../../../pages/types'
import { isAuthor } from '../../../utils/isAuthor'
import { CheckButton } from '../../_shared/CheckButton'
type SubscriptionFilter = 'all' | 'users' | 'topics'
type Props = { type Props = {
caption?: string caption?: string
hideWriteButton?: boolean hideWriteButton?: boolean
@ -40,10 +42,7 @@ type Props = {
followers?: Author[] followers?: Author[]
following?: Array<Author | Topic> following?: Array<Author | Topic>
showPublicationsCounter?: boolean showPublicationsCounter?: boolean
} minimizeSubscribeButton?: boolean
function isAuthor(value: Author | Topic): value is Author {
return 'name' in value
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
@ -276,12 +275,35 @@ export const AuthorCard = (props: Props) => {
<For each={props.author.links}>{(link) => <a href={link} />}</For> <For each={props.author.links}>{(link) => <a href={link} />}</For>
</div> </div>
</Show> </Show>
<Show when={!props.minimizeSubscribeButton}>
<Show <Show
when={subscribed()} when={subscribed()}
fallback={ 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>
}
>
<button <button
onClick={handleSubscribe} onClick={() => subscribe(false)}
class={clsx('button', styles.button)} class={clsx('button', styles.button)}
classList={{ classList={{
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton, [styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
@ -293,52 +315,37 @@ export const AuthorCard = (props: Props) => {
disabled={isSubscribing()} disabled={isSubscribing()}
> >
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}> <Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
<Icon name="author-subscribe" class={styles.icon} /> <Icon name="author-unsubscribe" class={styles.icon} />
</Show> </Show>
<Show when={props.isTextButton || props.isAuthorPage}> <Show when={props.isTextButton || props.isAuthorPage}>
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}> <span
{t('Follow')} class={clsx(
styles.buttonLabel,
styles.buttonLabelVisible,
styles.buttonUnfollowLabel
)}
>
{t('Unfollow')}
</span>
<span
class={clsx(
styles.buttonLabel,
styles.buttonLabelVisible,
styles.buttonSubscribedLabel
)}
>
{t('You are subscribed')}
</span> </span>
</Show> </Show>
</button> </button>
} </Show>
> </Show>
<button <Show when={props.minimizeSubscribeButton}>
<CheckButton
text={t('Follow')}
checked={subscribed()}
onClick={() => subscribe(false)} 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('You are subscribed')}
</span>
</Show>
</button>
</Show> </Show>
<Show when={!props.hideWriteButton}> <Show when={!props.hideWriteButton}>
@ -425,7 +432,7 @@ export const AuthorCard = (props: Props) => {
<div class="row"> <div class="row">
<div class="col-24"> <div class="col-24">
<For each={following()}> <For each={following()}>
{(subscription: Author | Topic) => {(subscription) =>
isAuthor(subscription) ? ( isAuthor(subscription) ? (
<AuthorCard <AuthorCard
author={subscription} author={subscription}

View File

@ -1,10 +0,0 @@
.navigationHeader {
@include font-size(1.8rem);
font-weight: bold;
margin-top: 1.1em;
}
.navigation {
@include font-size(1.4rem);
}

View File

@ -0,0 +1,19 @@
.navigationHeader {
@include font-size(1.8rem);
font-weight: bold;
margin-top: 1.1em;
}
.navigation {
@include font-size(1.4rem);
.active {
a {
text-decoration: none;
color: var(--default-color-invert);
background: var(--background-color-invert);
cursor: inherit;
}
}
}

View File

@ -1,20 +1,22 @@
import styles from './ProfileSettingsNavigation.module.scss' import styles from './ProfileSettingsNavigation.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router'
export default () => { export const ProfileSettingsNavigation = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter()
return ( return (
<> <>
<h4 class={styles.navigationHeader}>{t('Settings')}</h4> <h4 class={styles.navigationHeader}>{t('Settings')}</h4>
<ul class={clsx(styles.navigation, 'nodash')}> <ul class={clsx(styles.navigation, 'nodash')}>
<li> <li class={clsx({ [styles.active]: page().route === 'profileSettings' })}>
<a href="/profile/settings">{t('Profile')}</a> <a href="/profile/settings">{t('Profile')}</a>
</li> </li>
<li> <li class={clsx({ [styles.active]: page().route === 'profileSubscriptions' })}>
<a href="/profile/subscriptions">{t('Subscriptions')}</a> <a href="/profile/subscriptions">{t('Subscriptions')}</a>
</li> </li>
<li> <li class={clsx({ [styles.active]: page().route === 'profileSecurity' })}>
<a href="/profile/security">{t('Security')}</a> <a href="/profile/security">{t('Security')}</a>
</li> </li>
</ul> </ul>

View File

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

View File

@ -11,6 +11,7 @@ import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { CardTopic } from '../Feed/CardTopic' import { CardTopic } from '../Feed/CardTopic'
import { CheckButton } from '../_shared/CheckButton'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -24,6 +25,7 @@ interface TopicProps {
showPublications?: boolean showPublications?: boolean
showDescription?: boolean showDescription?: boolean
isCardMode?: boolean isCardMode?: boolean
minimizeSubscribeButton?: boolean
} }
export const TopicCard = (props: TopicProps) => { export const TopicCard = (props: TopicProps) => {
@ -105,27 +107,34 @@ export const TopicCard = (props: TopicProps) => {
> >
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={isSessionLoaded()}> <Show when={isSessionLoaded()}>
<button <Show
onClick={handleSubscribe} when={!props.minimizeSubscribeButton}
class="button--light button--subscribe-topic" fallback={
classList={{ <CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} />
[styles.isSubscribing]: isSubscribing(), }
[styles.isSubscribed]: subscribed()
}}
disabled={isSubscribing()}
> >
<Show when={props.iconButton}> <button
<Show when={subscribed()} fallback="+"> onClick={handleSubscribe}
<Icon name="check-subscribed" /> class="button--light button--subscribe-topic"
classList={{
[styles.isSubscribing]: isSubscribing(),
[styles.isSubscribed]: subscribed()
}}
disabled={isSubscribing()}
>
<Show when={props.iconButton}>
<Show when={subscribed()} fallback="+">
<Icon name="check-subscribed" />
</Show>
</Show> </Show>
</Show> <Show when={!props.iconButton}>
<Show when={!props.iconButton}> <Show when={subscribed()} fallback={t('Follow')}>
<Show when={subscribed()} fallback={t('Follow')}> <span class={styles.buttonUnfollowLabel}>{t('Unfollow')}</span>
<span class={styles.buttonUnfollowLabel}>{t('Unfollow')}</span> <span class={styles.buttonSubscribedLabel}>{t('You are subscribed')}</span>
<span class={styles.buttonSubscribedLabel}>{t('You are subscribed')}</span> </Show>
</Show> </Show>
</Show> </button>
</button> </Show>
</Show> </Show>
</ShowOnlyOnClient> </ShowOnlyOnClient>
</div> </div>

View File

@ -11,6 +11,7 @@ import styles from '../../styles/AllTopics.module.scss'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { dummyFilter } from '../../utils/dummyFilter'
type AllAuthorsPageSearchParams = { type AllAuthorsPageSearchParams = {
by: '' | 'name' | 'shouts' | 'followers' by: '' | 'name' | 'shouts' | 'followers'
@ -69,26 +70,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || '')) const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
const filteredAuthors = createMemo(() => { const filteredAuthors = createMemo(() => {
let q = searchQuery().toLowerCase() return dummyFilter(sortedAuthors(), searchQuery(), lang())
if (q.length === 0) {
return sortedAuthors()
}
if (lang() === 'ru') q = translit(q)
return sortedAuthors().filter((author) => {
if (author.slug.split('-').some((w) => w.startsWith(q))) {
return true
}
let name = author.name.toLowerCase()
if (lang() === 'ru') {
name = translit(name)
}
return name.split(' ').some((word) => word.startsWith(q))
})
}) })
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
@ -186,7 +168,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
<div class="row"> <div class="row">
<div class="col-lg-20 col-xl-18"> <div class="col-lg-20 col-xl-18">
<AuthorCard <AuthorCard
author={author} author={author as Author}
hasLink={true} hasLink={true}
subscribed={subscribed(author.slug)} subscribed={subscribed(author.slug)}
noSocialButtons={true} noSocialButtons={true}

View File

@ -12,6 +12,7 @@ import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { StatMetrics } from '../_shared/StatMetrics' import { StatMetrics } from '../_shared/StatMetrics'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { dummyFilter } from '../../utils/dummyFilter'
type AllTopicsPageSearchParams = { type AllTopicsPageSearchParams = {
by: 'shouts' | 'authors' | 'title' | '' by: 'shouts' | 'authors' | 'title' | ''
@ -66,32 +67,9 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || ''))
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
const filteredResults = createMemo(() => { const filteredResults = createMemo(() => {
/* very stupid filter by string algorithm with no deps */ return dummyFilter(sortedTopics(), searchQuery(), lang())
let q = searchQuery().toLowerCase()
if (q.length === 0) {
return sortedTopics()
}
if (lang() === 'ru') {
q = translit(q)
}
return sortedTopics().filter((topic) => {
if (topic.slug.split('-').some((w) => w.startsWith(q))) {
return true
}
let title = topic.title.toLowerCase()
if (lang() === 'ru') {
title = translit(title)
}
return title.split(' ').some((word) => word.startsWith(q))
})
}) })
const AllTopicsHead = () => ( const AllTopicsHead = () => (
@ -180,7 +158,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
{(topic) => ( {(topic) => (
<> <>
<TopicCard <TopicCard
topic={topic} topic={topic as Topic}
compact={false} compact={false}
subscribed={subscribed(topic.slug)} subscribed={subscribed(topic.slug)}
showPublications={true} showPublications={true}

View File

@ -23,7 +23,6 @@ type Props = {
shouts: Shout[] shouts: Shout[]
author: Author author: Author
authorSlug: string authorSlug: string
// route?: 'author' | 'authorComments' | 'authorAbout' | 'authorFollowing' | 'authorFollowers' | string
} }
export const PRERENDERED_ARTICLES_COUNT = 12 export const PRERENDERED_ARTICLES_COUNT = 12
@ -40,11 +39,11 @@ export const AuthorView = (props: Props) => {
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
const [followers, setFollowers] = createSignal<Author[]>([]) const [followers, setFollowers] = createSignal<Author[]>([])
const [subscriptions, setSubscriptions] = createSignal<Array<Author | Topic>>([]) const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
const [bioWrapper, setBioWrapper] = createSignal<HTMLElement>()
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const bioContainerRef: { current: HTMLDivElement } = { current: null } const bioContainerRef: { current: HTMLDivElement } = { current: null }
const bioWrapperRef: { current: HTMLDivElement } = { current: null }
const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => { const fetchSubscriptions = async (): Promise<{ authors: Author[]; topics: Topic[] }> => {
try { try {
const [getAuthors, getTopics] = await Promise.all([ const [getAuthors, getTopics] = await Promise.all([
@ -62,7 +61,7 @@ export const AuthorView = (props: Props) => {
const checkBioHeight = () => { const checkBioHeight = () => {
if (bioContainerRef.current) { if (bioContainerRef.current) {
setShowExpandBioControl(bioContainerRef.current.offsetHeight > bioWrapper().offsetHeight) setShowExpandBioControl(bioContainerRef.current.offsetHeight > bioWrapperRef.current.offsetHeight)
} }
} }
@ -81,7 +80,7 @@ export const AuthorView = (props: Props) => {
await loadMore() await loadMore()
} }
const { authors, topics } = await fetchSubscriptions() const { authors, topics } = await fetchSubscriptions()
setSubscriptions([...authors, ...topics]) setFollowing([...authors, ...topics])
}) })
const loadMore = async () => { const loadMore = async () => {
@ -131,7 +130,7 @@ export const AuthorView = (props: Props) => {
author={author()} author={author()}
isAuthorPage={true} isAuthorPage={true}
followers={followers()} followers={followers()}
following={subscriptions()} following={following()}
/> />
</Show> </Show>
<div class={clsx(styles.groupControls, 'row')}> <div class={clsx(styles.groupControls, 'row')}>
@ -170,7 +169,7 @@ export const AuthorView = (props: Props) => {
<div class="row"> <div class="row">
<div class="col-md-20 col-lg-18"> <div class="col-md-20 col-lg-18">
<div <div
ref={setBioWrapper} ref={(el) => (bioWrapperRef.current = el)}
class={styles.longBio} class={styles.longBio}
classList={{ [styles.longBioExpanded]: isBioExpanded() }} classList={{ [styles.longBioExpanded]: isBioExpanded() }}
> >

View File

@ -0,0 +1,30 @@
.CheckButton {
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
min-width: 33px;
box-sizing: border-box;
padding: 0 8px;
background: var(--background-color);
color: var(--default-color);
border: 2px solid var(--default-color);
border-radius: 8px;
overflow: hidden;
align-self: center;
.close {
display: none;
}
&:hover {
background: var(--background-color-invert);
color: var(--default-color-invert);
.check {
display: none;
}
.close {
display: block;
}
}
}

View File

@ -0,0 +1,38 @@
import { clsx } from 'clsx'
import styles from './CheckButton.module.scss'
import { Icon } from '../Icon'
import { createSignal, Show } from 'solid-js'
type Props = {
class?: string
checked: boolean
text: string
onClick: () => void
}
// Signed - check mark icon
// On hover - cross icon
// If you clicked on the cross, you unsubscribed. Then the “Subscribe” button appears
export const CheckButton = (props: Props) => {
const [clicked, setClicked] = createSignal(!props.checked)
const handleClick = () => {
props.onClick()
setClicked((prev) => !prev)
}
return (
<button type="button" class={clsx(styles.CheckButton, props.class)} onClick={handleClick}>
<Show
when={clicked()}
fallback={
<>
<Icon name="check-subscribed-black" class={styles.check} />
<Icon name="close-white" class={styles.close} />
</>
}
>
{props.text}
</Show>
</button>
)
}

View File

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

View File

@ -1,6 +1,23 @@
.searchField { .searchField {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
position: relative;
&.bordered {
border: 2px solid var(--black-100);
padding: 10px 0 12px 10px;
input {
width: 100%;
display: block;
box-sizing: border-box;
margin-right: 40px;
&:focus {
box-shadow: unset;
}
}
}
input { input {
border: none; border: none;

View File

@ -1,19 +1,20 @@
import styles from './SearchField.module.scss' import styles from './SearchField.module.scss'
import { Icon } from './Icon' import { Icon } from '../Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../../context/localize'
type SearchFieldProps = { type Props = {
onChange: (value: string) => void onChange: (value: string) => void
class?: string class?: string
variant?: 'bordered'
} }
export const SearchField = (props: SearchFieldProps) => { export const SearchField = (props: Props) => {
const handleInputChange = (event) => props.onChange(event.target.value.trim()) const handleInputChange = (event) => props.onChange(event.target.value.trim())
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<div class={clsx(styles.searchField, props.class)}> <div class={clsx(styles.searchField, props.class, { [styles.bordered]: props.variant === 'bordered' })}>
<label for="search-field"> <label for="search-field">
<Icon name="search" class={styles.icon} /> <Icon name="search" class={styles.icon} />
</label> </label>
@ -24,7 +25,7 @@ export const SearchField = (props: SearchFieldProps) => {
onInput={handleInputChange} onInput={handleInputChange}
placeholder={t('Search')} placeholder={t('Search')}
/> />
<label for="search-field">Поиск</label> <label for="search-field">{t('Search')}</label>
</div> </div>
) )
} }

View File

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

View File

@ -5,6 +5,12 @@ import { loadAuthor, useAuthorsStore } from '../stores/zine/authors'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import type { ProfileInput } from '../graphql/types.gen' import type { ProfileInput } from '../graphql/types.gen'
const userpicUrl = (userpic: string) => {
if (userpic.includes('assets.discours.io')) {
return userpic.replace('100x', '500x500')
}
return userpic
}
const useProfileForm = () => { const useProfileForm = () => {
const { session } = useSession() const { session } = useSession()
const currentSlug = createMemo(() => session()?.user?.slug) const currentSlug = createMemo(() => session()?.user?.slug)
@ -34,13 +40,12 @@ const useProfileForm = () => {
if (!currentSlug()) return if (!currentSlug()) return
try { try {
await loadAuthor({ slug: currentSlug() }) await loadAuthor({ slug: currentSlug() })
setForm({ setForm({
name: currentAuthor()?.name, name: currentAuthor()?.name,
slug: currentAuthor()?.slug, slug: currentAuthor()?.slug,
bio: currentAuthor()?.bio, bio: currentAuthor()?.bio,
about: currentAuthor()?.about, about: currentAuthor()?.about,
userpic: currentAuthor()?.userpic.replace('100x', '500x500'), userpic: userpicUrl(currentAuthor()?.userpic),
links: currentAuthor()?.links links: currentAuthor()?.links
}) })
} catch (error) { } catch (error) {

View File

@ -108,12 +108,12 @@ h5 {
} }
.searchField { .searchField {
display: block; margin-bottom: 2rem;
label:first-child { label:first-child {
opacity: 0.5; opacity: 0.5;
position: absolute; position: absolute;
right: 1em; right: 12px;
transform: translateY(-50%); transform: translateY(-50%);
top: 50%; top: 50%;
} }

View File

@ -2,7 +2,7 @@ import { PageLayout } from '../../components/_shared/PageLayout'
import styles from './Settings.module.scss' import styles from './Settings.module.scss'
import { Icon } from '../../components/_shared/Icon' import { Icon } from '../../components/_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import ProfileSettingsNavigation from '../../components/Discours/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
export const ProfileSecurityPage = () => { export const ProfileSecurityPage = () => {
return ( return (

View File

@ -1,6 +1,6 @@
import { PageLayout } from '../../components/_shared/PageLayout' import { PageLayout } from '../../components/_shared/PageLayout'
import { Icon } from '../../components/_shared/Icon' import { Icon } from '../../components/_shared/Icon'
import ProfileSettingsNavigation from '../../components/Discours/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
import { For, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js' import { For, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { clsx } from 'clsx' import { clsx } from 'clsx'

View File

@ -2,10 +2,61 @@ import { PageLayout } from '../../components/_shared/PageLayout'
import styles from './Settings.module.scss' import styles from './Settings.module.scss'
import stylesSettings from '../../styles/FeedSettings.module.scss' import stylesSettings from '../../styles/FeedSettings.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import ProfileSettingsNavigation from '../../components/Discours/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
import { SearchField } from '../../components/_shared/SearchField' import { SearchField } from '../../components/_shared/SearchField'
import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import { Author, Topic } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient'
import { useSession } from '../../context/session'
import { isAuthor } from '../../utils/isAuthor'
import { useLocalize } from '../../context/localize'
import { SubscriptionFilter } from '../types'
import { Loading } from '../../components/_shared/Loading'
import { TopicCard } from '../../components/Topic/Card'
import { AuthorCard } from '../../components/Author/AuthorCard'
import { dummyFilter } from '../../utils/dummyFilter'
export const ProfileSubscriptionsPage = () => { export const ProfileSubscriptionsPage = () => {
const { t, lang } = useLocalize()
const { user, isAuthenticated } = useSession()
const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [searchQuery, setSearchQuery] = createSignal('')
const fetchSubscriptions = async () => {
try {
const [getAuthors, getTopics] = await Promise.all([
apiClient.getAuthorFollowingUsers({ slug: user().slug }),
apiClient.getAuthorFollowingTopics({ slug: user().slug })
])
setFollowing([...getAuthors, ...getTopics])
setFiltered([...getAuthors, ...getTopics])
} catch (error) {
console.error('[fetchSubscriptions] :', error)
throw error
}
}
onMount(async () => {
if (isAuthenticated()) {
await fetchSubscriptions()
}
})
createEffect(() => {
if (following()) {
if (subscriptionFilter() === 'users') {
setFiltered(following().filter((s) => 'name' in s))
} else if (subscriptionFilter() === 'topics') {
setFiltered(following().filter((s) => 'title' in s))
} else {
setFiltered(following())
}
}
setFiltered(dummyFilter(following(), searchQuery(), lang()))
})
return ( return (
<PageLayout> <PageLayout>
<div class="wide-container"> <div class="wide-container">
@ -19,112 +70,65 @@ export const ProfileSubscriptionsPage = () => {
<div class="col-md-19"> <div class="col-md-19">
<div class="row"> <div class="row">
<div class="col-md-20 col-lg-18 col-xl-16"> <div class="col-md-20 col-lg-18 col-xl-16">
<h1>Подписки</h1> <h1>{t('My subscriptions')}</h1>
<p class="description">Здесь можно управлять всеми своими подписками на&nbsp;сайте.</p> <p class="description">{t('Here you can manage all your Discourse subscriptions')}</p>
<Show when={following()} fallback={<Loading />}>
<form>
<ul class="view-switcher"> <ul class="view-switcher">
<li class="selected"> <li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
<a href="src/components/Pages/profile#">Все</a> <button type="button" onClick={() => setSubscriptionFilter('all')}>
{t('All')}
</button>
</li> </li>
<li> <li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
<a href="src/components/Pages/profile#">Авторы</a> <button type="button" onClick={() => setSubscriptionFilter('users')}>
{t('Authors')}
</button>
</li> </li>
<li> <li
<a href="src/components/Pages/profile#">Темы</a> class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}
</li> >
<li> <button type="button" onClick={() => setSubscriptionFilter('topics')}>
<a href="src/components/Pages/profile#">Сообщества</a> {t('Topics')}
</li> </button>
<li>
<a href="src/components/Pages/profile#">Коллекции</a>
</li> </li>
</ul> </ul>
<div class={clsx('pretty-form__item', styles.searchContainer)}> <div class={clsx('pretty-form__item', styles.searchContainer)}>
<SearchField onChange={() => console.log('nothing')} class={styles.searchField} /> <SearchField
onChange={(value) => setSearchQuery(value)}
class={styles.searchField}
variant="bordered"
/>
</div> </div>
<div class={clsx(stylesSettings.settingsList, styles.topicsList)}> <div class={clsx(stylesSettings.settingsList, styles.topicsList)}>
<div class={stylesSettings.settingsListRow}> <For each={filtered()}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}> {(followingItem) => (
<input type="checkbox" name="checkbox1" id="checkbox1" /> <div>
<label for="checkbox1" /> {isAuthor(followingItem) ? (
</div> <AuthorCard
<label for="checkbox1" class={stylesSettings.settingsListCell}> author={followingItem}
Культура hideWriteButton={true}
</label> hasLink={true}
</div> isTextButton={true}
<div class={stylesSettings.settingsListRow}> truncateBio={true}
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}> minimizeSubscribeButton={true}
<input type="checkbox" name="checkbox2" id="checkbox2" /> />
<label for="checkbox2" /> ) : (
</div> <TopicCard
<label for="checkbox2" class={stylesSettings.settingsListCell}> compact
Eto_ya sam isTopicInRow
</label> showDescription
</div> isCardMode
<div class={stylesSettings.settingsListRow}> topic={followingItem}
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}> minimizeSubscribeButton={true}
<input type="checkbox" name="checkbox3" id="checkbox3" /> />
<label for="checkbox3" /> )}
</div> </div>
<label for="checkbox3" class={stylesSettings.settingsListCell}> )}
Технопарк </For>
</label>
</div>
<div class={stylesSettings.settingsListRow}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}>
<input type="checkbox" name="checkbox4" id="checkbox4" />
<label for="checkbox4" />
</div>
<label for="checkbox4" class={stylesSettings.settingsListCell}>
Лучшее
</label>
</div>
<div class={stylesSettings.settingsListRow}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}>
<input type="checkbox" name="checkbox5" id="checkbox5" />
<label for="checkbox5" />
</div>
<label for="checkbox5" class={stylesSettings.settingsListCell}>
Реклама
</label>
</div>
<div class={stylesSettings.settingsListRow}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}>
<input type="checkbox" name="checkbox6" id="checkbox6" />
<label for="checkbox6" />
</div>
<label for="checkbox6" class={stylesSettings.settingsListCell}>
Искусство
</label>
</div>
<div class={stylesSettings.settingsListRow}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}>
<input type="checkbox" name="checkbox7" id="checkbox7" />
<label for="checkbox7" />
</div>
<label for="checkbox7" class={stylesSettings.settingsListCell}>
Общество
</label>
</div>
<div class={stylesSettings.settingsListRow}>
<div class={clsx(stylesSettings.settingsListCell, styles.topicsListItem)}>
<input type="checkbox" name="checkbox8" id="checkbox8" />
<label for="checkbox8" />
</div>
<label for="checkbox8" class={stylesSettings.settingsListCell}>
Личный опыт
</label>
</div>
</div> </div>
</Show>
<br />
<p>
<button class="button button--submit">Сохранить настройки</button>
</p>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -46,3 +46,5 @@ export type UploadedFile = {
url: string url: string
originalFilename?: string originalFilename?: string
} }
export type SubscriptionFilter = 'all' | 'users' | 'topics'

View File

@ -1,5 +1,6 @@
.settingsList { .settingsList {
display: table; display: table;
width: 100%;
h2 { h2 {
margin-top: 1em; margin-top: 1em;
@ -45,16 +46,3 @@
} }
} }
} }
.settingsListRow {
display: table-row;
}
.settingsListCell {
display: table-cell;
padding: 0 0.5em 1em 0;
&:first-child {
padding-right: 2em;
}
}

36
src/utils/dummyFilter.ts Normal file
View File

@ -0,0 +1,36 @@
import { translit } from './ru2en'
import { Author, Topic } from '../graphql/types.gen'
type SearchData = Array<Author | Topic>
const prepareQuery = (searchQuery, lang) => {
const q = searchQuery.toLowerCase()
if (q.length === 0) return ''
return lang === 'ru' ? translit(q) : q
}
const stringMatches = (str, q, lang) => {
const preparedStr = lang === 'ru' ? translit(str.toLowerCase()) : str.toLowerCase()
return preparedStr.split(' ').some((word) => word.startsWith(q))
}
export const dummyFilter = (data: SearchData, searchQuery: string, lang: 'ru' | 'en'): SearchData => {
const q = prepareQuery(searchQuery, lang)
if (q.length === 0) return data
return data.filter((item) => {
const slugMatches = item.slug && item.slug.split('-').some((w) => w.startsWith(q))
if (slugMatches) return true
if ('title' in item) {
return stringMatches(item.title, q, lang)
}
if ('name' in item) {
return stringMatches(item.name, q, lang) || (item.bio && stringMatches(item.bio, q, lang))
}
// If it does not match any of the 'slug', 'title', 'name' , 'bio' fields
// current element should not be included in the filtered array
return false
})
}

5
src/utils/isAuthor.ts Normal file
View File

@ -0,0 +1,5 @@
import { Author, Topic } from '../graphql/types.gen'
export const isAuthor = (value: Author | Topic): value is Author => {
return 'name' in value
}