my feed with auth guard (#245)

* my feed with auth guard

* header links css fix

* don't create history record when reseting lng param

---------

Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
Ilya Y 2023-09-29 15:48:58 +03:00 committed by GitHub
parent 18ec665bb2
commit aadc9677a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 328 additions and 240 deletions

25
package-lock.json generated
View File

@ -64,6 +64,7 @@
"@tiptap/extension-text": "2.0.3",
"@tiptap/extension-underline": "2.0.3",
"@tiptap/extension-youtube": "2.0.3",
"@types/js-cookie": "3.0.4",
"@types/node": "20.1.1",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",
@ -5155,6 +5156,12 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/js-cookie": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.4.tgz",
"integrity": "sha512-vMMnFF+H5KYqdd/myCzq6wLDlPpteJK+jGFgBus3Da7lw+YsDmx2C8feGTzY2M3Fo823yON+HC2CL240j4OV+w==",
"dev": true
},
"node_modules/@types/js-yaml": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
@ -5249,9 +5256,9 @@
}
},
"node_modules/@types/yargs": {
"version": "17.0.25",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.25.tgz",
"integrity": "sha512-gy7iPgwnzNvxgAEi2bXOHWCVOG6f7xsprVJH4MjlAWeBmJ7vh/Y1kwMtUrs64ztf24zVIRCpr3n/z6gm9QIkgg==",
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz",
"integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
@ -21885,6 +21892,12 @@
"@types/istanbul-lib-report": "*"
}
},
"@types/js-cookie": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.4.tgz",
"integrity": "sha512-vMMnFF+H5KYqdd/myCzq6wLDlPpteJK+jGFgBus3Da7lw+YsDmx2C8feGTzY2M3Fo823yON+HC2CL240j4OV+w==",
"dev": true
},
"@types/js-yaml": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
@ -21979,9 +21992,9 @@
}
},
"@types/yargs": {
"version": "17.0.25",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.25.tgz",
"integrity": "sha512-gy7iPgwnzNvxgAEi2bXOHWCVOG6f7xsprVJH4MjlAWeBmJ7vh/Y1kwMtUrs64ztf24zVIRCpr3n/z6gm9QIkgg==",
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.26.tgz",
"integrity": "sha512-Y3vDy2X6zw/ZCumcwLpdhM5L7jmyGpmBCTYMHDLqT2IKVMYRRLdv6ZakA+wxhra6Z/3bwhNbNl9bDGXaFU+6rw==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"

View File

@ -84,6 +84,7 @@
"@tiptap/extension-text": "2.0.3",
"@tiptap/extension-underline": "2.0.3",
"@tiptap/extension-youtube": "2.0.3",
"@types/js-cookie": "3.0.4",
"@types/node": "20.1.1",
"@typescript-eslint/eslint-plugin": "6.7.3",
"@typescript-eslint/parser": "6.7.3",

View File

@ -1,7 +1,7 @@
// FIXME: breaks on vercel, research
// import 'solid-devtools'
import { MODALS, showModal } from '../stores/ui'
import { hideModal, MODALS, showModal } from '../stores/ui'
import { Component, createEffect, createMemo } from 'solid-js'
import { ROUTES, useRouter } from '../stores/router'
import { Dynamic } from 'solid-js/web'
@ -89,6 +89,10 @@ export const App = (props: PageProps) => {
const { page, searchParams } = useRouter<RootSearchParams>()
createEffect(() => {
if (!searchParams().modal) {
hideModal()
}
const modal = MODALS[searchParams().modal]
if (modal) {
showModal(modal)

View File

@ -97,7 +97,9 @@ export const FullArticle = (props: Props) => {
createEffect(() => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments()
changeSearchParam('scrollTo', null)
changeSearchParam({
scrollTo: null
})
}
})

View File

@ -1,6 +1,9 @@
import { createEffect, JSX, Show } from 'solid-js'
import { useSession } from '../../context/session'
import { hideModal, showModal } from '../../stores/ui'
import { useRouter } from '../../stores/router'
import { RootSearchParams } from '../../pages/types'
import { AuthModalSearchParams } from '../Nav/AuthModal/types'
type Props = {
children: JSX.Element
@ -9,6 +12,7 @@ type Props = {
export const AuthGuard = (props: Props) => {
const { isAuthenticated, isSessionLoaded } = useSession()
const { changeSearchParam } = useRouter<RootSearchParams & AuthModalSearchParams>()
createEffect(() => {
if (props.disabled) {
@ -18,7 +22,13 @@ export const AuthGuard = (props: Props) => {
if (isAuthenticated()) {
hideModal()
} else {
showModal('auth', 'authguard')
changeSearchParam(
{
source: 'authguard',
modal: 'auth'
},
true
)
}
}
})

View File

@ -93,7 +93,9 @@ export const AuthorCard = (props: Props) => {
const initChat = () => {
requireAuthentication(() => {
openPage(router, `inbox`)
changeSearchParam('initChat', `${props.author.id}`)
changeSearchParam({
initChat: props.author.id.toString()
})
}, 'discussions')
}

View File

@ -28,7 +28,9 @@ export default () => {
class="button"
onClick={() => {
showModal('auth')
changeSearchParam('mode', 'register')
changeSearchParam({
mode: 'register'
})
}}
>
{t('Join the community')}

View File

@ -84,7 +84,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
const scrollToComments = (event) => {
event.preventDefault()
openPage(router, 'article', { slug: props.article.slug })
changeSearchParam('scrollTo', 'comments')
changeSearchParam({
scrollTo: 'comments'
})
}
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)

View File

@ -115,7 +115,9 @@ export const ForgotPasswordForm = () => {
href="#"
onClick={(event) => {
event.preventDefault()
changeSearchParam('mode', 'register')
changeSearchParam({
mode: 'register'
})
}}
>
{t('register')}
@ -132,7 +134,14 @@ export const ForgotPasswordForm = () => {
</button>
</div>
<div class={styles.authControl}>
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
<span
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'login'
})
}
>
{t('I know the password')}
</span>
</div>

View File

@ -195,11 +195,12 @@ export const LoginForm = () => {
</div>
<div class={styles.authActions}>
<span
class={'link'}
onClick={(ev) => {
ev.preventDefault()
changeSearchParam('mode', 'forgot-password')
}}
class="link"
onClick={() =>
changeSearchParam({
mode: 'forgot-password'
})
}
>
{t('Forgot password?')}
</span>
@ -210,7 +211,14 @@ export const LoginForm = () => {
<SocialProviders />
<div class={styles.authControl}>
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'register')}>
<span
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'register'
})
}
>
{t('I have no account yet')}
</span>
</div>

View File

@ -198,7 +198,9 @@ export const RegisterForm = () => {
href="#"
onClick={(event) => {
event.preventDefault()
changeSearchParam('mode', 'login')
changeSearchParam({
mode: 'login'
})
}}
>
{t('enter')}
@ -246,7 +248,14 @@ export const RegisterForm = () => {
<SocialProviders />
<div class={styles.authControl}>
<span class={styles.authLink} onClick={() => changeSearchParam('mode', 'login')}>
<span
class={styles.authLink}
onClick={() =>
changeSearchParam({
mode: 'login'
})
}
>
{t('I have an account')}
</span>
</div>

View File

@ -251,7 +251,7 @@
.mainNavigationItemActive {
background: var(--link-hover-background);
color: var(--link-hover-color);
color: var(--link-hover-color) !important;
}
.headerWithTitle.headerScrolledBottom {

View File

@ -40,15 +40,9 @@ export const ProfilePopup = (props: ProfilePopupProps) => {
<a href={getPagePath(router, 'profileSettings')}>{t('Settings')}</a>
</li>
<li class={styles.topBorderItem}>
<a
href="#"
onClick={(event) => {
event.preventDefault()
signOut()
}}
>
<span class="link" onClick={() => signOut()}>
{t('Logout')}
</a>
</span>
</li>
</ul>
</Popup>

View File

@ -38,7 +38,9 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
onMount(() => {
if (!searchParams().by) {
changeSearchParam('by', 'shouts')
changeSearchParam({
by: 'shouts'
})
}
})
@ -47,7 +49,8 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
})
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce((acc, author) => {
return sortedAuthors().reduce(
(acc, author) => {
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@'
@ -56,7 +59,9 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
acc[letter].push(author)
return acc
}, {} as { [letter: string]: Author[] })
},
{} as { [letter: string]: Author[] }
)
})
const sortedKeys = createMemo<string[]>(() => {

View File

@ -38,7 +38,9 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
onMount(() => {
if (!searchParams().by) {
changeSearchParam('by', 'shouts')
changeSearchParam({
by: 'shouts'
})
}
})
@ -47,13 +49,16 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
})
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
return sortedTopics().reduce((acc, topic) => {
return sortedTopics().reduce(
(acc, topic) => {
let letter = topic.title[0].toUpperCase()
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '#'
if (!acc[letter]) acc[letter] = []
acc[letter].push(topic)
return acc
}, {} as { [letter: string]: Topic[] })
},
{} as { [letter: string]: Topic[] }
)
})
const sortedKeys = createMemo<string[]>(() => {

View File

@ -3,13 +3,13 @@ import { Icon } from '../_shared/Icon'
import { ArticleCard } from '../Feed/ArticleCard'
import { AuthorCard } from '../Author/AuthorCard'
import { Sidebar } from '../Feed/Sidebar'
import { loadShouts, loadMyFeed, useArticlesStore, resetSortedArticles } from '../../stores/zine/articles'
import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { clsx } from 'clsx'
import { useReactions } from '../../context/reactions'
import type { Author, LoadShoutsOptions, Reaction } from '../../graphql/types.gen'
import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/types.gen'
import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../stores/router'
import { useLocalize } from '../../context/localize'
@ -18,8 +18,6 @@ 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 { AuthGuard } from '../AuthGuard'
import { useSession } from '../../context/session'
export const FEED_PAGE_SIZE = 20
@ -39,17 +37,13 @@ const getOrderBy = (by: FeedSearchParams['by']) => {
return ''
}
const routesWithAuthGuard = new Set([
'feedMy',
'feedNotifications',
'feedBookmarks',
'feedCollaborations',
'feedDiscussions'
])
export const FeedView = () => {
type Props = {
loadShouts: (options: LoadShoutsOptions) => Promise<{ hasMore: boolean; newShouts: Shout[] }>
}
export const FeedView = (props: Props) => {
const { t } = useLocalize()
const { page, searchParams } = useRouter<FeedSearchParams>()
const { isAuthenticated } = useSession()
const [isLoading, setIsLoading] = createSignal(false)
// state
@ -91,15 +85,7 @@ export const FeedView = () => {
options.order_by = orderBy
}
if (isAuthenticated()) {
return loadMyFeed(options)
}
// default feed
return loadShouts({
...options,
filters: { visibility: 'community' }
})
return props.loadShouts(options)
}
const loadMore = async () => {
@ -124,8 +110,6 @@ export const FeedView = () => {
})
return (
<div>
<AuthGuard disabled={!routesWithAuthGuard.has(page().route)}>
<div class="wide-container feed">
<div class="row">
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
@ -136,8 +120,7 @@ export const FeedView = () => {
<ul class={clsx(styles.feedFilter, 'view-switcher')}>
<li
class={clsx({
'view-switcher__item--selected':
searchParams().by === 'publish_date' || !searchParams().by
'view-switcher__item--selected': searchParams().by === 'publish_date' || !searchParams().by
})}
>
<a href={getPagePath(router, page().route)}>{t('Recent')}</a>
@ -275,7 +258,5 @@ export const FeedView = () => {
</aside>
</div>
</div>
</AuthGuard>
</div>
)
}

View File

@ -64,7 +64,9 @@ export const InboxView = () => {
const handleOpenChat = async (chat: Chat) => {
setCurrentDialog(chat)
changeSearchParam('chat', `${chat.id}`)
changeSearchParam({
chat: chat.id
})
try {
await getMessages(chat.id)
} catch (error) {
@ -121,8 +123,10 @@ export const InboxView = () => {
try {
const newChat = await createChat([Number(searchParams().initChat)], '')
await loadChats()
changeSearchParam('initChat', null)
changeSearchParam('chat', newChat.chat.id)
changeSearchParam({
initChat: null,
chat: newChat.chat.id
})
const chatToOpen = chats().find((chat) => chat.id === newChat.chat.id)
await handleOpenChat(chatToOpen)
} catch (error) {

View File

@ -88,7 +88,14 @@ export const TopicView = (props: TopicProps) => {
'view-switcher__item--selected': searchParams().by === 'recent' || !searchParams().by
}}
>
<button type="button" onClick={() => changeSearchParam('by', 'recent')}>
<button
type="button"
onClick={() =>
changeSearchParam({
by: 'recent'
})
}
>
{t('Recent')}
</button>
</li>

View File

@ -33,7 +33,7 @@ export const LocalizeProvider = (props: { children: JSX.Element }) => {
changeLanguage(lng)
setLang(lng)
Cookie.set('lng', lng)
changeSearchParam('lng', null)
changeSearchParam({ lng: null }, true)
})
const value: LocalizeContextType = { t, lang, setLang }

View File

@ -74,8 +74,8 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
const signIn = async ({ email, password }: { email: string; password: string }) => {
const authResult = await apiClient.authLogin({ email, password })
mutate(authResult)
setToken(authResult.token)
mutate(authResult)
console.debug('signed in')
}

View File

@ -1,16 +1,41 @@
import { PageLayout } from '../components/_shared/PageLayout'
import { FeedView } from '../components/Views/Feed'
import { onCleanup } from 'solid-js'
import { resetSortedArticles } from '../stores/zine/articles'
import { Match, onCleanup, Switch } from 'solid-js'
import { loadMyFeed, loadShouts, resetSortedArticles } from '../stores/zine/articles'
import { ReactionsProvider } from '../context/reactions'
import { useRouter } from '../stores/router'
import { AuthGuard } from '../components/AuthGuard'
import { LoadShoutsOptions } from '../graphql/types.gen'
export const FeedPage = () => {
onCleanup(() => resetSortedArticles())
const { page } = useRouter()
const handleFeedLoadShouts = (options: LoadShoutsOptions) => {
return loadShouts({
...options,
filters: { visibility: 'community' }
})
}
const handleMyFeedLoadShouts = (options: LoadShoutsOptions) => {
return loadMyFeed(options)
}
return (
<PageLayout>
<ReactionsProvider>
<FeedView />
<Switch fallback={<FeedView loadShouts={handleFeedLoadShouts} />}>
<Match when={page().route === 'feed'}>
<FeedView loadShouts={handleFeedLoadShouts} />
</Match>
<Match when={page().route === 'feedMy'}>
<AuthGuard>
<FeedView loadShouts={handleMyFeedLoadShouts} />
</AuthGuard>
</Match>
</Switch>
</ReactionsProvider>
</PageLayout>
)

View File

@ -137,17 +137,16 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
const page = useStore(routerStore)
const searchParams = useStore(searchParamsStore) as unknown as Accessor<TSearchParams>
const changeSearchParam = <TKey extends keyof TSearchParams>(
key: TKey,
value: TSearchParams[TKey],
replace = false
) => {
const changeSearchParam = (newValues: Partial<TSearchParams>, replace = false) => {
const newSearchParams = { ...searchParamsStore.get() }
if (value === null) {
Object.keys(newValues).forEach((key) => {
if (newValues[key] === null) {
delete newSearchParams[key.toString()]
} else {
newSearchParams[key.toString()] = value
newSearchParams[key.toString()] = newValues[key]
}
})
searchParamsStore.open(newSearchParams, replace)
}

View File

@ -58,7 +58,9 @@ const { searchParams, changeSearchParam } = useRouter<
export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) => {
if (modalSource) {
changeSearchParam('source', modalSource)
changeSearchParam({
source: modalSource
})
}
setModal(modalType)
@ -66,15 +68,19 @@ export const showModal = (modalType: ModalType, modalSource?: AuthModalSource) =
// TODO: find a better solution
export const hideModal = () => {
if (searchParams().modal === 'auth') {
if (searchParams().mode === 'confirm-email') {
changeSearchParam('token', null, true)
}
changeSearchParam('mode', null, true)
const newSearchParams: Partial<AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams> = {
modal: null,
source: null
}
changeSearchParam('modal', null, true)
changeSearchParam('source', null)
if (searchParams().modal === 'auth') {
if (searchParams().mode === 'confirm-email') {
newSearchParams.token = null
}
newSearchParams.mode = null
}
changeSearchParam(newSearchParams, true)
setModal(null)
}