Add AuthWrapper (#227)

* Add AuthWrapper
This commit is contained in:
Ilya Y 2023-09-21 14:38:22 +03:00 committed by GitHub
parent a9b67ff9ff
commit 5e18abba2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 521 additions and 466 deletions

View File

@ -0,0 +1,3 @@
.AuthGuard {
min-height: 60vh;
}

View 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>
}

View File

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

View File

@ -1,5 +1,12 @@
export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-password' 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 = { export type AuthModalSearchParams = {
mode: AuthModalMode mode: AuthModalMode

View File

@ -145,6 +145,7 @@ export const Header = (props: Props) => {
const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT }) const topics = await apiClient.getRandomTopics({ amount: RANDOM_TOPICS_COUNT })
setRandomTopics(topics) setRandomTopics(topics)
}) })
return ( return (
<header <header
class={styles.mainHeader} class={styles.mainHeader}
@ -156,7 +157,12 @@ export const Header = (props: Props) => {
[styles.headerWithTitle]: Boolean(props.title) [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 /> <AuthModal />
</Modal> </Modal>

View File

@ -107,10 +107,7 @@ export const HeaderAuth = (props: Props) => {
return ( return (
<ShowOnlyOnClient> <ShowOnlyOnClient>
<Show when={isSessionLoaded()} keyed={true}> <Show when={isSessionLoaded()} keyed={true}>
<div <div class={clsx('col-sm-6 col-lg-7', styles.usernav)}>
class={clsx('col-sm-6 col-lg-7', styles.usernav)}
classList={{ [styles.usernavEditor]: showSaveButton() }}
>
<div class={styles.userControl}> <div class={styles.userControl}>
<Show when={showCreatePostButton()}> <Show when={showCreatePostButton()}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>

View File

@ -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 type { JSX } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { hideModal, useModalStore } from '../../../stores/ui' import { hideModal, useModalStore } from '../../../stores/ui'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import styles from './Modal.module.scss' import styles from './Modal.module.scss'
interface ModalProps { interface Props {
name: string name: string
variant: 'narrow' | 'wide' variant: 'narrow' | 'wide'
children: JSX.Element children: JSX.Element
onClose?: () => void onClose?: () => void
noPadding?: boolean noPadding?: boolean
maxHeight?: boolean maxHeight?: boolean
allowClose?: boolean
} }
export const Modal = (props: ModalProps) => { export const Modal = (props: Props) => {
const { modal } = useModalStore() const { modal } = useModalStore()
const [visible, setVisible] = createSignal(false) const [visible, setVisible] = createSignal(false)
const allowClose = createMemo(() => props.allowClose !== false)
const handleHide = () => { const handleHide = () => {
if (modal()) { if (modal() && allowClose()) {
hideModal() hideModal()
props.onClose && props.onClose() props.onClose && props.onClose()
} }
@ -46,6 +45,7 @@ export const Modal = (props: ModalProps) => {
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
{props.children} {props.children}
<Show when={allowClose()}>
<div class={styles.close} onClick={handleHide}> <div class={styles.close} onClick={handleHide}>
<svg <svg
class={styles.icon} class={styles.icon}
@ -60,6 +60,7 @@ export const Modal = (props: ModalProps) => {
/> />
</svg> </svg>
</div> </div>
</Show>
</div> </div>
</div> </div>
</Show> </Show>

View File

@ -0,0 +1,3 @@
.ProfileSubscriptions {
display: block;
}

View File

@ -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>
)
}

View File

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

View File

@ -8,6 +8,7 @@ import { apiClient } from '../utils/apiClient'
import { redirectPage } from '@nanostores/router' import { redirectPage } from '@nanostores/router'
import { router } from '../stores/router' import { router } from '../stores/router'
import { LayoutType } from './types' import { LayoutType } from './types'
import { AuthGuard } from '../components/AuthGuard'
const handleCreate = async (layout: LayoutType) => { const handleCreate = async (layout: LayoutType) => {
const shout = await apiClient.createArticle({ article: { layout: layout } }) const shout = await apiClient.createArticle({ article: { layout: layout } })
@ -20,6 +21,7 @@ export const CreatePage = () => {
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<PageLayout> <PageLayout>
<AuthGuard>
<article class={clsx('wide-container', 'container--static-page', styles.Create)}> <article class={clsx('wide-container', 'container--static-page', styles.Create)}>
<h1>{t('Choose a post type')}</h1> <h1>{t('Choose a post type')}</h1>
<ul class={clsx('nodash', styles.list)}> <ul class={clsx('nodash', styles.list)}>
@ -56,6 +58,7 @@ export const CreatePage = () => {
</ul> </ul>
<Button value={t('Back')} onClick={() => window.history.back()} /> <Button value={t('Back')} onClick={() => window.history.back()} />
</article> </article>
</AuthGuard>
</PageLayout> </PageLayout>
) )
} }

View File

@ -6,6 +6,7 @@ import { Shout } from '../graphql/types.gen'
import { useRouter } from '../stores/router' import { useRouter } from '../stores/router'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
import { AuthGuard } from '../components/AuthGuard'
const Edit = lazy(() => import('../components/Views/Edit')) const Edit = lazy(() => import('../components/Views/Edit'))
@ -26,24 +27,13 @@ export const EditPage = () => {
return ( return (
<PageLayout> <PageLayout>
<Show when={isSessionLoaded()}> <AuthGuard>
<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()}> <Show when={shout()}>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<Edit shout={shout()} /> <Edit shout={shout()} />
</Suspense> </Suspense>
</Show> </Show>
</Show> </AuthGuard>
</Show>
</PageLayout> </PageLayout>
) )
} }

View File

@ -3,10 +3,12 @@ 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/Nav/ProfileSettingsNavigation' import { ProfileSettingsNavigation } from '../../components/Nav/ProfileSettingsNavigation'
import { AuthGuard } from '../../components/AuthGuard'
export const ProfileSecurityPage = () => { export const ProfileSecurityPage = () => {
return ( return (
<PageLayout> <PageLayout>
<AuthGuard>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<div class="col-md-5"> <div class="col-md-5">
@ -128,6 +130,7 @@ export const ProfileSecurityPage = () => {
</div> </div>
</div> </div>
</div> </div>
</AuthGuard>
</PageLayout> </PageLayout>
) )
} }

View File

@ -18,6 +18,7 @@ import { createStore } from 'solid-js/store'
import { clone } from '../../utils/clone' import { clone } from '../../utils/clone'
import SimplifiedEditor from '../../components/Editor/SimplifiedEditor' import SimplifiedEditor from '../../components/Editor/SimplifiedEditor'
import { GrowingTextarea } from '../../components/_shared/GrowingTextarea' import { GrowingTextarea } from '../../components/_shared/GrowingTextarea'
import { AuthGuard } from '../../components/AuthGuard'
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const { t } = useLocalize() const { t } = useLocalize()
@ -106,6 +107,7 @@ export const ProfileSettingsPage = () => {
return ( return (
<PageLayout> <PageLayout>
<AuthGuard>
<Show when={form}> <Show when={form}>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
@ -213,7 +215,11 @@ export const ProfileSettingsPage = () => {
<div class={clsx(styles.multipleControls, 'pretty-form__item')}> <div class={clsx(styles.multipleControls, 'pretty-form__item')}>
<div class={styles.multipleControlsHeader}> <div class={styles.multipleControlsHeader}>
<h4>{t('Social networks')}</h4> <h4>{t('Social networks')}</h4>
<button type="button" class="button" onClick={() => setAddLinkForm(!addLinkForm())}> <button
type="button"
class="button"
onClick={() => setAddLinkForm(!addLinkForm())}
>
+ +
</button> </button>
</div> </div>
@ -257,6 +263,7 @@ export const ProfileSettingsPage = () => {
</div> </div>
</div> </div>
</Show> </Show>
</AuthGuard>
</PageLayout> </PageLayout>
) )
} }

View File

@ -1,142 +1,13 @@
import { PageLayout } from '../../components/_shared/PageLayout' import { PageLayout } from '../../components/_shared/PageLayout'
import styles from './Settings.module.scss' import { AuthGuard } from '../../components/AuthGuard'
import stylesSettings from '../../styles/FeedSettings.module.scss' import { ProfileSubscriptions } from '../../components/Views/ProfileSubscriptions'
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'
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(() => {
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 ( return (
<PageLayout> <PageLayout>
<div class="wide-container"> <AuthGuard>
<div class="row"> <ProfileSubscriptions />
<div class="col-md-5"> </AuthGuard>
<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>
</PageLayout> </PageLayout>
) )
} }

View File

@ -48,7 +48,7 @@ export const MODALS: Record<ModalType, ModalType> = {
following: 'following' following: 'following'
} }
const [modal, setModal] = createSignal<ModalType | null>(null) const [modal, setModal] = createSignal<ModalType | null>()
const [warnings, setWarnings] = createSignal<Warning[]>([]) const [warnings, setWarnings] = createSignal<Warning[]>([])