parent
a9b67ff9ff
commit
5e18abba2a
3
src/components/AuthGuard/AuthGuard.module.scss
Normal file
3
src/components/AuthGuard/AuthGuard.module.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.AuthGuard {
|
||||
min-height: 60vh;
|
||||
}
|
22
src/components/AuthGuard/AuthGuard.tsx
Normal file
22
src/components/AuthGuard/AuthGuard.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { createEffect, JSX, Show } from 'solid-js'
|
||||
import { useSession } from '../../context/session'
|
||||
import { hideModal, showModal } from '../../stores/ui'
|
||||
|
||||
type Props = {
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export const AuthGuard = (props: Props) => {
|
||||
const { isAuthenticated, isSessionLoaded } = useSession()
|
||||
createEffect(() => {
|
||||
if (isSessionLoaded()) {
|
||||
if (isAuthenticated()) {
|
||||
hideModal()
|
||||
} else {
|
||||
showModal('auth', 'authguard')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return <Show when={isSessionLoaded() && isAuthenticated()}>{props.children}</Show>
|
||||
}
|
1
src/components/AuthGuard/index.ts
Normal file
1
src/components/AuthGuard/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AuthGuard } from './AuthGuard'
|
|
@ -1,5 +1,12 @@
|
|||
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password'
|
||||
export type AuthModalSource = 'discussions' | 'vote' | 'subscribe' | 'bookmark' | 'follow' | 'create'
|
||||
export type AuthModalSource =
|
||||
| 'discussions'
|
||||
| 'vote'
|
||||
| 'subscribe'
|
||||
| 'bookmark'
|
||||
| 'follow'
|
||||
| 'create'
|
||||
| 'authguard'
|
||||
|
||||
export type AuthModalSearchParams = {
|
||||
mode: AuthModalMode
|
||||
|
|
|
@ -145,6 +145,7 @@ export const Header = (props: Props) => {
|
|||
const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT })
|
||||
setRandomTopics(topics)
|
||||
})
|
||||
|
||||
return (
|
||||
<header
|
||||
class={styles.mainHeader}
|
||||
|
@ -156,7 +157,12 @@ export const Header = (props: Props) => {
|
|||
[styles.headerWithTitle]: Boolean(props.title)
|
||||
}}
|
||||
>
|
||||
<Modal variant={searchParams().source ? 'narrow' : 'wide'} name="auth" noPadding={true}>
|
||||
<Modal
|
||||
variant={searchParams().source ? 'narrow' : 'wide'}
|
||||
name="auth"
|
||||
allowClose={searchParams().source !== 'authguard'}
|
||||
noPadding={true}
|
||||
>
|
||||
<AuthModal />
|
||||
</Modal>
|
||||
|
||||
|
|
|
@ -107,10 +107,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
return (
|
||||
<ShowOnlyOnClient>
|
||||
<Show when={isSessionLoaded()} keyed={true}>
|
||||
<div
|
||||
class={clsx('col-sm-6 col-lg-7', styles.usernav)}
|
||||
classList={{ [styles.usernavEditor]: showSaveButton() }}
|
||||
>
|
||||
<div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
|
||||
<div class={styles.userControl}>
|
||||
<Show when={showCreatePostButton()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
import { createEffect, createSignal, Show } from 'solid-js'
|
||||
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
|
||||
import type { JSX } from 'solid-js'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
import { hideModal, useModalStore } from '../../../stores/ui'
|
||||
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
|
||||
|
||||
import styles from './Modal.module.scss'
|
||||
|
||||
interface ModalProps {
|
||||
interface Props {
|
||||
name: string
|
||||
variant: 'narrow' | 'wide'
|
||||
children: JSX.Element
|
||||
onClose?: () => void
|
||||
noPadding?: boolean
|
||||
maxHeight?: boolean
|
||||
allowClose?: boolean
|
||||
}
|
||||
|
||||
export const Modal = (props: ModalProps) => {
|
||||
export const Modal = (props: Props) => {
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const [visible, setVisible] = createSignal(false)
|
||||
|
||||
const allowClose = createMemo(() => props.allowClose !== false)
|
||||
const handleHide = () => {
|
||||
if (modal()) {
|
||||
if (modal() && allowClose()) {
|
||||
hideModal()
|
||||
props.onClose && props.onClose()
|
||||
}
|
||||
|
@ -46,20 +45,22 @@ export const Modal = (props: ModalProps) => {
|
|||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{props.children}
|
||||
<div class={styles.close} onClick={handleHide}>
|
||||
<svg
|
||||
class={styles.icon}
|
||||
width="16"
|
||||
height="18"
|
||||
viewBox="0 0 16 18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.99987 7.52552L14.1871 0.92334L15.9548 2.80968L9.76764 9.41185L15.9548 16.014L14.1871 17.9004L7.99987 11.2982L1.81269 17.9004L0.0449219 16.014L6.23211 9.41185L0.0449225 2.80968L1.81269 0.92334L7.99987 7.52552Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Show when={allowClose()}>
|
||||
<div class={styles.close} onClick={handleHide}>
|
||||
<svg
|
||||
class={styles.icon}
|
||||
width="16"
|
||||
height="18"
|
||||
viewBox="0 0 16 18"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M7.99987 7.52552L14.1871 0.92334L15.9548 2.80968L9.76764 9.41185L15.9548 16.014L14.1871 17.9004L7.99987 11.2982L1.81269 17.9004L0.0449219 16.014L6.23211 9.41185L0.0449225 2.80968L1.81269 0.92334L7.99987 7.52552Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.ProfileSubscriptions {
|
||||
display: block;
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import { clsx } from 'clsx'
|
||||
// import styles from './ProfileSubscriptions.module.scss'
|
||||
import { ProfileSettingsNavigation } from '../../Nav/ProfileSettingsNavigation'
|
||||
import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { SearchField } from '../../_shared/SearchField'
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
import { AuthorCard } from '../../Author/AuthorCard'
|
||||
import { TopicCard } from '../../Topic/Card'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { Author, Topic } from '../../../graphql/types.gen'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { apiClient } from '../../../utils/apiClient'
|
||||
import { dummyFilter } from '../../../utils/dummyFilter'
|
||||
// TODO: refactor styles
|
||||
import styles from '../../../pages/profile/Settings.module.scss'
|
||||
import stylesSettings from '../../../styles/FeedSettings.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const ProfileSubscriptions = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const { user } = 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
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
if (searchQuery()) {
|
||||
setFiltered(dummyFilter(following(), searchQuery(), lang()))
|
||||
}
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
await fetchSubscriptions()
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>{t('My subscriptions')}</h1>
|
||||
<p class="description">{t('Here you can manage all your Discourse subscriptions')}</p>
|
||||
<Show when={following()} fallback={<Loading />}>
|
||||
<ul class="view-switcher">
|
||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||
{t('All')}
|
||||
</button>
|
||||
</li>
|
||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
||||
{t('Authors')}
|
||||
</button>
|
||||
</li>
|
||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||
{t('Topics')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class={clsx('pretty-form__item', styles.searchContainer)}>
|
||||
<SearchField
|
||||
onChange={(value) => setSearchQuery(value)}
|
||||
class={styles.searchField}
|
||||
variant="bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={clsx(stylesSettings.settingsList, styles.topicsList)}>
|
||||
<For each={filtered()}>
|
||||
{(followingItem) => (
|
||||
<div>
|
||||
{isAuthor(followingItem) ? (
|
||||
<AuthorCard
|
||||
author={followingItem}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
isTextButton={true}
|
||||
truncateBio={true}
|
||||
minimizeSubscribeButton={true}
|
||||
/>
|
||||
) : (
|
||||
<TopicCard
|
||||
compact
|
||||
isTopicInRow
|
||||
showDescription
|
||||
isCardMode
|
||||
topic={followingItem}
|
||||
minimizeSubscribeButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Views/ProfileSubscriptions/index.ts
Normal file
1
src/components/Views/ProfileSubscriptions/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { ProfileSubscriptions } from './ProfileSubscriptions'
|
|
@ -8,6 +8,7 @@ import { apiClient } from '../utils/apiClient'
|
|||
import { redirectPage } from '@nanostores/router'
|
||||
import { router } from '../stores/router'
|
||||
import { LayoutType } from './types'
|
||||
import { AuthGuard } from '../components/AuthGuard'
|
||||
|
||||
const handleCreate = async (layout: LayoutType) => {
|
||||
const shout = await apiClient.createArticle({ article: { layout: layout } })
|
||||
|
@ -20,42 +21,44 @@ export const CreatePage = () => {
|
|||
const { t } = useLocalize()
|
||||
return (
|
||||
<PageLayout>
|
||||
<article class={clsx('wide-container', 'container--static-page', styles.Create)}>
|
||||
<h1>{t('Choose a post type')}</h1>
|
||||
<ul class={clsx('nodash', styles.list)}>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('article')}>
|
||||
<Icon name="create-article" class={styles.icon} />
|
||||
<div>{t('article')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('literature')}>
|
||||
<Icon name="create-books" class={styles.icon} />
|
||||
<div>{t('literature')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('image')}>
|
||||
<Icon name="create-images" class={styles.icon} />
|
||||
<div>{t('images')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('audio')}>
|
||||
<Icon name="create-music" class={styles.icon} />
|
||||
<div>{t('music')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('video')}>
|
||||
<Icon name="create-video" class={styles.icon} />
|
||||
<div>{t('video')}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<Button value={t('Back')} onClick={() => window.history.back()} />
|
||||
</article>
|
||||
<AuthGuard>
|
||||
<article class={clsx('wide-container', 'container--static-page', styles.Create)}>
|
||||
<h1>{t('Choose a post type')}</h1>
|
||||
<ul class={clsx('nodash', styles.list)}>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('article')}>
|
||||
<Icon name="create-article" class={styles.icon} />
|
||||
<div>{t('article')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('literature')}>
|
||||
<Icon name="create-books" class={styles.icon} />
|
||||
<div>{t('literature')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('image')}>
|
||||
<Icon name="create-images" class={styles.icon} />
|
||||
<div>{t('images')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('audio')}>
|
||||
<Icon name="create-music" class={styles.icon} />
|
||||
<div>{t('music')}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class={styles.link} onClick={() => handleCreate('video')}>
|
||||
<Icon name="create-video" class={styles.icon} />
|
||||
<div>{t('video')}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<Button value={t('Back')} onClick={() => window.history.back()} />
|
||||
</article>
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Shout } from '../graphql/types.gen'
|
|||
import { useRouter } from '../stores/router'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
import { useLocalize } from '../context/localize'
|
||||
import { AuthGuard } from '../components/AuthGuard'
|
||||
|
||||
const Edit = lazy(() => import('../components/Views/Edit'))
|
||||
|
||||
|
@ -26,24 +27,13 @@ export const EditPage = () => {
|
|||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Show when={isSessionLoaded()}>
|
||||
<Show
|
||||
when={isAuthenticated()}
|
||||
fallback={
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">{t("Let's log in")}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={shout()}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Edit shout={shout()} />
|
||||
</Suspense>
|
||||
</Show>
|
||||
<AuthGuard>
|
||||
<Show when={shout()}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Edit shout={shout()} />
|
||||
</Suspense>
|
||||
</Show>
|
||||
</Show>
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,131 +3,134 @@ import styles from './Settings.module.scss'
|
|||
import { Icon } from '../../components/_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
|
||||
import { AuthGuard } from '../../components/AuthGuard'
|
||||
|
||||
export const ProfileSecurityPage = () => {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
<AuthGuard>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>Вход и безопасность</h1>
|
||||
<p class="description">Настройки аккаунта, почты, пароля и способов входа.</p>
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>Вход и безопасность</h1>
|
||||
<p class="description">Настройки аккаунта, почты, пароля и способов входа.</p>
|
||||
|
||||
<form>
|
||||
<h4>Почта</h4>
|
||||
<div class="pretty-form__item">
|
||||
<input type="text" name="email" id="email" placeholder="Почта" />
|
||||
<label for="email">Почта</label>
|
||||
</div>
|
||||
<form>
|
||||
<h4>Почта</h4>
|
||||
<div class="pretty-form__item">
|
||||
<input type="text" name="email" id="email" placeholder="Почта" />
|
||||
<label for="email">Почта</label>
|
||||
</div>
|
||||
|
||||
<h4>Изменить пароль</h4>
|
||||
<h5>Текущий пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="text"
|
||||
name="password-current"
|
||||
id="password-current"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-hide" />
|
||||
</button>
|
||||
</div>
|
||||
<h4>Изменить пароль</h4>
|
||||
<h5>Текущий пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="text"
|
||||
name="password-current"
|
||||
id="password-current"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-hide" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h5>Новый пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="password"
|
||||
name="password-new"
|
||||
id="password-new"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-open" />
|
||||
</button>
|
||||
</div>
|
||||
<h5>Новый пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="password"
|
||||
name="password-new"
|
||||
id="password-new"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-open" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h5>Подтвердите новый пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="password"
|
||||
name="password-new-confirm"
|
||||
id="password-new-confirm"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-open" />
|
||||
</button>
|
||||
</div>
|
||||
<h5>Подтвердите новый пароль</h5>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="password"
|
||||
name="password-new-confirm"
|
||||
id="password-new-confirm"
|
||||
class={clsx(styles.passwordInput, 'nolabel')}
|
||||
/>
|
||||
<button type="button" class={styles.passwordToggleControl}>
|
||||
<Icon name="password-open" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h4>Социальные сети</h4>
|
||||
<h5>Google</h5>
|
||||
<div class="pretty-form__item">
|
||||
<h4>Социальные сети</h4>
|
||||
<h5>Google</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx('button', 'button--light', styles.socialButton)} type="button">
|
||||
<Icon name="google" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>VK</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="vk" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>Facebook</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="facebook" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>Apple</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button
|
||||
class={clsx(
|
||||
styles.socialButton,
|
||||
styles.socialButtonApple,
|
||||
'button' + ' button--light'
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="apple" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
<button class={clsx('button', 'button--light', styles.socialButton)} type="button">
|
||||
<Icon name="google" class={styles.icon} />
|
||||
Привязать
|
||||
<button class="button button--submit" type="submit">
|
||||
Сохранить настройки
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>VK</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="vk" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>Facebook</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button class={clsx(styles.socialButton, 'button', 'button--light')} type="button">
|
||||
<Icon name="facebook" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h5>Apple</h5>
|
||||
<div class="pretty-form__item">
|
||||
<p>
|
||||
<button
|
||||
class={clsx(
|
||||
styles.socialButton,
|
||||
styles.socialButtonApple,
|
||||
'button' + ' button--light'
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
<Icon name="apple" class={styles.icon} />
|
||||
Привязать
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
<button class="button button--submit" type="submit">
|
||||
Сохранить настройки
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { createStore } from 'solid-js/store'
|
|||
import { clone } from '../../utils/clone'
|
||||
import SimplifiedEditor from '../../components/Editor/SimplifiedEditor'
|
||||
import { GrowingTextarea } from '../../components/_shared/GrowingTextarea'
|
||||
import { AuthGuard } from '../../components/AuthGuard'
|
||||
|
||||
export const ProfileSettingsPage = () => {
|
||||
const { t } = useLocalize()
|
||||
|
@ -106,157 +107,163 @@ export const ProfileSettingsPage = () => {
|
|||
|
||||
return (
|
||||
<PageLayout>
|
||||
<Show when={form}>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
<AuthGuard>
|
||||
<Show when={form}>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>{t('Profile settings')}</h1>
|
||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||
<form onSubmit={handleSubmit} enctype="multipart/form-data">
|
||||
<h4>{t('Userpic')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<Userpic
|
||||
name={form.name}
|
||||
userpic={form.userpic}
|
||||
isBig={true}
|
||||
onClick={handleAvatarClick}
|
||||
loading={isUserpicUpdating()}
|
||||
/>
|
||||
</div>
|
||||
<h4>{t('Name')}</h4>
|
||||
<p class="description">
|
||||
{t(
|
||||
'Your name will appear on your profile page and as your signature in publications, comments and responses.'
|
||||
)}
|
||||
</p>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder={t('Name')}
|
||||
onChange={(event) => updateFormField('name', event.currentTarget.value)}
|
||||
value={form.name}
|
||||
/>
|
||||
<label for="username">{t('Name')}</label>
|
||||
</div>
|
||||
|
||||
<h4>{t('Address on Discourse')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<div class={styles.discoursName}>
|
||||
<label for="user-address">https://{hostname()}/author/</label>
|
||||
<div class={styles.discoursNameField}>
|
||||
<input
|
||||
type="text"
|
||||
name="user-address"
|
||||
id="user-address"
|
||||
onChange={(event) => updateFormField('slug', event.currentTarget.value)}
|
||||
value={form.slug}
|
||||
class="nolabel"
|
||||
/>
|
||||
<Show when={slugError()}>
|
||||
<p class="form-message form-message--error">{t(`${slugError()}`)}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>{t('Profile settings')}</h1>
|
||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||
<form onSubmit={handleSubmit} enctype="multipart/form-data">
|
||||
<h4>{t('Userpic')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<Userpic
|
||||
name={form.name}
|
||||
userpic={form.userpic}
|
||||
isBig={true}
|
||||
onClick={handleAvatarClick}
|
||||
loading={isUserpicUpdating()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{t('Introduce')}</h4>
|
||||
<GrowingTextarea
|
||||
variant="bordered"
|
||||
placeholder={t('Introduce')}
|
||||
value={(value) => updateFormField('bio', value)}
|
||||
initialValue={form.bio}
|
||||
allowEnterKey={false}
|
||||
maxLength={80}
|
||||
/>
|
||||
|
||||
<h4>{t('About myself')}</h4>
|
||||
<SimplifiedEditor
|
||||
variant="bordered"
|
||||
onlyBubbleControls={true}
|
||||
smallHeight={true}
|
||||
placeholder={t('About myself')}
|
||||
label={t('About myself')}
|
||||
initialContent={form.about}
|
||||
onChange={(value) => updateFormField('about', value)}
|
||||
/>
|
||||
{/*Нет реализации полей на бэке*/}
|
||||
{/*<h4>{t('How can I help/skills')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input type="text" name="skills" id="skills" />*/}
|
||||
{/*</div>*/}
|
||||
{/*<h4>{t('Where')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input type="text" name="location" id="location" placeholder="Откуда" />*/}
|
||||
{/* <label for="location">{t('Where')}</label>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
{/*<h4>{t('Date of Birth')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input*/}
|
||||
{/* type="date"*/}
|
||||
{/* name="birthdate"*/}
|
||||
{/* id="birthdate"*/}
|
||||
{/* placeholder="Дата рождения"*/}
|
||||
{/* class="nolabel"*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
|
||||
<div class={styles.multipleControlsHeader}>
|
||||
<h4>{t('Social networks')}</h4>
|
||||
<button type="button" class="button" onClick={() => setAddLinkForm(!addLinkForm())}>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<Show when={addLinkForm()}>
|
||||
<div class={styles.multipleControlsItem}>
|
||||
<input
|
||||
autofocus={true}
|
||||
type="text"
|
||||
name="link"
|
||||
class="nolabel"
|
||||
onChange={(event) => handleChangeSocial(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<Show when={incorrectUrl()}>
|
||||
<p class="form-message form-message--error">{t('It does not look like url')}</p>
|
||||
</Show>
|
||||
</Show>
|
||||
<For each={form.links}>
|
||||
{(link) => (
|
||||
<div class={styles.multipleControlsItem}>
|
||||
<input type="text" value={link} readonly={true} name="link" class="nolabel" />
|
||||
<button type="button" onClick={() => updateFormField('links', link, true)}>
|
||||
<Icon name="remove" class={styles.icon} />
|
||||
</button>
|
||||
</div>
|
||||
<h4>{t('Name')}</h4>
|
||||
<p class="description">
|
||||
{t(
|
||||
'Your name will appear on your profile page and as your signature in publications, comments and responses.'
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<br />
|
||||
<FloatingPanel
|
||||
isVisible={isFloatingPanelVisible()}
|
||||
confirmTitle={t('Save settings')}
|
||||
confirmAction={handleSaveProfile}
|
||||
declineTitle={t('Cancel')}
|
||||
declineAction={() => setIsFloatingPanelVisible(false)}
|
||||
/>
|
||||
</form>
|
||||
</p>
|
||||
<div class="pretty-form__item">
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder={t('Name')}
|
||||
onChange={(event) => updateFormField('name', event.currentTarget.value)}
|
||||
value={form.name}
|
||||
/>
|
||||
<label for="username">{t('Name')}</label>
|
||||
</div>
|
||||
|
||||
<h4>{t('Address on Discourse')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<div class={styles.discoursName}>
|
||||
<label for="user-address">https://{hostname()}/author/</label>
|
||||
<div class={styles.discoursNameField}>
|
||||
<input
|
||||
type="text"
|
||||
name="user-address"
|
||||
id="user-address"
|
||||
onChange={(event) => updateFormField('slug', event.currentTarget.value)}
|
||||
value={form.slug}
|
||||
class="nolabel"
|
||||
/>
|
||||
<Show when={slugError()}>
|
||||
<p class="form-message form-message--error">{t(`${slugError()}`)}</p>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>{t('Introduce')}</h4>
|
||||
<GrowingTextarea
|
||||
variant="bordered"
|
||||
placeholder={t('Introduce')}
|
||||
value={(value) => updateFormField('bio', value)}
|
||||
initialValue={form.bio}
|
||||
allowEnterKey={false}
|
||||
maxLength={80}
|
||||
/>
|
||||
|
||||
<h4>{t('About myself')}</h4>
|
||||
<SimplifiedEditor
|
||||
variant="bordered"
|
||||
onlyBubbleControls={true}
|
||||
smallHeight={true}
|
||||
placeholder={t('About myself')}
|
||||
label={t('About myself')}
|
||||
initialContent={form.about}
|
||||
onChange={(value) => updateFormField('about', value)}
|
||||
/>
|
||||
{/*Нет реализации полей на бэке*/}
|
||||
{/*<h4>{t('How can I help/skills')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input type="text" name="skills" id="skills" />*/}
|
||||
{/*</div>*/}
|
||||
{/*<h4>{t('Where')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input type="text" name="location" id="location" placeholder="Откуда" />*/}
|
||||
{/* <label for="location">{t('Where')}</label>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
{/*<h4>{t('Date of Birth')}</h4>*/}
|
||||
{/*<div class="pretty-form__item">*/}
|
||||
{/* <input*/}
|
||||
{/* type="date"*/}
|
||||
{/* name="birthdate"*/}
|
||||
{/* id="birthdate"*/}
|
||||
{/* placeholder="Дата рождения"*/}
|
||||
{/* class="nolabel"*/}
|
||||
{/* />*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<div class={clsx(styles.multipleControls, 'pretty-form__item')}>
|
||||
<div class={styles.multipleControlsHeader}>
|
||||
<h4>{t('Social networks')}</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
onClick={() => setAddLinkForm(!addLinkForm())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<Show when={addLinkForm()}>
|
||||
<div class={styles.multipleControlsItem}>
|
||||
<input
|
||||
autofocus={true}
|
||||
type="text"
|
||||
name="link"
|
||||
class="nolabel"
|
||||
onChange={(event) => handleChangeSocial(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<Show when={incorrectUrl()}>
|
||||
<p class="form-message form-message--error">{t('It does not look like url')}</p>
|
||||
</Show>
|
||||
</Show>
|
||||
<For each={form.links}>
|
||||
{(link) => (
|
||||
<div class={styles.multipleControlsItem}>
|
||||
<input type="text" value={link} readonly={true} name="link" class="nolabel" />
|
||||
<button type="button" onClick={() => updateFormField('links', link, true)}>
|
||||
<Icon name="remove" class={styles.icon} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<br />
|
||||
<FloatingPanel
|
||||
isVisible={isFloatingPanelVisible()}
|
||||
confirmTitle={t('Save settings')}
|
||||
confirmAction={handleSaveProfile}
|
||||
declineTitle={t('Cancel')}
|
||||
declineAction={() => setIsFloatingPanelVisible(false)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,142 +1,13 @@
|
|||
import { PageLayout } from '../../components/_shared/PageLayout'
|
||||
import styles from './Settings.module.scss'
|
||||
import stylesSettings from '../../styles/FeedSettings.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
|
||||
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'
|
||||
import { AuthGuard } from '../../components/AuthGuard'
|
||||
import { ProfileSubscriptions } from '../../components/Views/ProfileSubscriptions'
|
||||
|
||||
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(() => {
|
||||
console.log('!!! subscriptionFilter():', subscriptionFilter())
|
||||
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())
|
||||
}
|
||||
}
|
||||
if (searchQuery()) {
|
||||
setFiltered(dummyFilter(following(), searchQuery(), lang()))
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<div class={clsx('left-navigation', styles.leftNavigation)}>
|
||||
<ProfileSettingsNavigation />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-19">
|
||||
<div class="row">
|
||||
<div class="col-md-20 col-lg-18 col-xl-16">
|
||||
<h1>{t('My subscriptions')}</h1>
|
||||
<p class="description">{t('Here you can manage all your Discourse subscriptions')}</p>
|
||||
<Show when={following()} fallback={<Loading />}>
|
||||
<ul class="view-switcher">
|
||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'all' })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('all')}>
|
||||
{t('All')}
|
||||
</button>
|
||||
</li>
|
||||
<li class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'users' })}>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('users')}>
|
||||
{t('Authors')}
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({ 'view-switcher__item--selected': subscriptionFilter() === 'topics' })}
|
||||
>
|
||||
<button type="button" onClick={() => setSubscriptionFilter('topics')}>
|
||||
{t('Topics')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class={clsx('pretty-form__item', styles.searchContainer)}>
|
||||
<SearchField
|
||||
onChange={(value) => setSearchQuery(value)}
|
||||
class={styles.searchField}
|
||||
variant="bordered"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={clsx(stylesSettings.settingsList, styles.topicsList)}>
|
||||
<For each={filtered()}>
|
||||
{(followingItem) => (
|
||||
<div>
|
||||
{isAuthor(followingItem) ? (
|
||||
<AuthorCard
|
||||
author={followingItem}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
isTextButton={true}
|
||||
truncateBio={true}
|
||||
minimizeSubscribeButton={true}
|
||||
/>
|
||||
) : (
|
||||
<TopicCard
|
||||
compact
|
||||
isTopicInRow
|
||||
showDescription
|
||||
isCardMode
|
||||
topic={followingItem}
|
||||
minimizeSubscribeButton={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AuthGuard>
|
||||
<ProfileSubscriptions />
|
||||
</AuthGuard>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const MODALS: Record<ModalType, ModalType> = {
|
|||
following: 'following'
|
||||
}
|
||||
|
||||
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||
const [modal, setModal] = createSignal<ModalType | null>()
|
||||
|
||||
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user