diff --git a/package-lock.json b/package-lock.json index 05c4cd7f..207b6dd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": "*" diff --git a/package.json b/package.json index c7443705..082da1e1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/App.tsx b/src/components/App.tsx index ebd77789..0e352b16 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -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() createEffect(() => { + if (!searchParams().modal) { + hideModal() + } + const modal = MODALS[searchParams().modal] if (modal) { showModal(modal) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 4eefa9bf..9dc14a94 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -97,7 +97,9 @@ export const FullArticle = (props: Props) => { createEffect(() => { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { scrollToComments() - changeSearchParam('scrollTo', null) + changeSearchParam({ + scrollTo: null + }) } }) diff --git a/src/components/AuthGuard/AuthGuard.tsx b/src/components/AuthGuard/AuthGuard.tsx index 5fa27636..5a2394e2 100644 --- a/src/components/AuthGuard/AuthGuard.tsx +++ b/src/components/AuthGuard/AuthGuard.tsx @@ -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() 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 + ) } } }) diff --git a/src/components/Author/AuthorCard/AuthorCard.tsx b/src/components/Author/AuthorCard/AuthorCard.tsx index 43b464a8..ee877937 100644 --- a/src/components/Author/AuthorCard/AuthorCard.tsx +++ b/src/components/Author/AuthorCard/AuthorCard.tsx @@ -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') } diff --git a/src/components/Discours/Hero.tsx b/src/components/Discours/Hero.tsx index f4b0701c..9a1425af 100644 --- a/src/components/Discours/Hero.tsx +++ b/src/components/Discours/Hero.tsx @@ -28,7 +28,9 @@ export default () => { class="button" onClick={() => { showModal('auth') - changeSearchParam('mode', 'register') + changeSearchParam({ + mode: 'register' + }) }} > {t('Join the community')} diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx index 3b4ed6ff..cb5b1b58 100644 --- a/src/components/Feed/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard.tsx @@ -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) diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index 48d58603..4d28d027 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -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 = () => {
- changeSearchParam('mode', 'login')}> + + changeSearchParam({ + mode: 'login' + }) + } + > {t('I know the password')}
diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index c3d4c5a4..f468d230 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -195,11 +195,12 @@ export const LoginForm = () => {
{ - ev.preventDefault() - changeSearchParam('mode', 'forgot-password') - }} + class="link" + onClick={() => + changeSearchParam({ + mode: 'forgot-password' + }) + } > {t('Forgot password?')} @@ -210,7 +211,14 @@ export const LoginForm = () => {
- changeSearchParam('mode', 'register')}> + + changeSearchParam({ + mode: 'register' + }) + } + > {t('I have no account yet')}
diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 5f27e539..152c0ad5 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -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 = () => {
- changeSearchParam('mode', 'login')}> + + changeSearchParam({ + mode: 'login' + }) + } + > {t('I have an account')}
diff --git a/src/components/Nav/Header/Header.module.scss b/src/components/Nav/Header/Header.module.scss index 5173d486..b678c1bd 100644 --- a/src/components/Nav/Header/Header.module.scss +++ b/src/components/Nav/Header/Header.module.scss @@ -251,7 +251,7 @@ .mainNavigationItemActive { background: var(--link-hover-background); - color: var(--link-hover-color); + color: var(--link-hover-color) !important; } .headerWithTitle.headerScrolledBottom { diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index 718c1078..c6d7ec35 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -40,15 +40,9 @@ export const ProfilePopup = (props: ProfilePopupProps) => { {t('Settings')}
  • - { - event.preventDefault() - signOut() - }} - > + signOut()}> {t('Logout')} - +
  • diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 32af01f0..f9949bf9 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -38,7 +38,9 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { onMount(() => { if (!searchParams().by) { - changeSearchParam('by', 'shouts') + changeSearchParam({ + by: 'shouts' + }) } }) @@ -47,16 +49,19 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => { }) const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { - return sortedAuthors().reduce((acc, author) => { - let letter = author.name.trim().split(' ').pop().at(0).toUpperCase() + return sortedAuthors().reduce( + (acc, author) => { + let letter = author.name.trim().split(' ').pop().at(0).toUpperCase() - if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@' + if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@' - if (!acc[letter]) acc[letter] = [] + if (!acc[letter]) acc[letter] = [] - acc[letter].push(author) - return acc - }, {} as { [letter: string]: Author[] }) + acc[letter].push(author) + return acc + }, + {} as { [letter: string]: Author[] } + ) }) const sortedKeys = createMemo(() => { diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 65f73459..d8ec3491 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -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) => { - 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[] }) + 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[] } + ) }) const sortedKeys = createMemo(() => { diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index 8bbbbab0..c37e4aae 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -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() - 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,158 +110,153 @@ export const FeedView = () => { }) return ( -
    - -
    -
    -
    - -
    +
    +
    +
    + +
    -
    - +
    + - }> - 0}> - - {(article) => } - + }> + 0}> + + {(article) => } + -
    -
    -

    {t('Popular authors')}

    - - {t('All authors')} - - -
    +
    +
    +

    {t('Popular authors')}

    + + {t('All authors')} + + +
    -
      - - {(author) => ( -
    • - -
    • - )} -
      -
    -
    - - - {(article) => } - - - - -

    - -

    -
    - -
    - - -
    +
    + + + {(article) => } + + + + +

    + +

    +
    +
    - + + +
    ) } diff --git a/src/components/Views/Inbox.tsx b/src/components/Views/Inbox.tsx index ba8b6dca..eebe3edf 100644 --- a/src/components/Views/Inbox.tsx +++ b/src/components/Views/Inbox.tsx @@ -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) { diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index bf22cfee..cbdf8ff9 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -88,7 +88,14 @@ export const TopicView = (props: TopicProps) => { 'view-switcher__item--selected': searchParams().by === 'recent' || !searchParams().by }} > - diff --git a/src/context/localize.tsx b/src/context/localize.tsx index 2e7dab1f..166da80b 100644 --- a/src/context/localize.tsx +++ b/src/context/localize.tsx @@ -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 } diff --git a/src/context/session.tsx b/src/context/session.tsx index da759e9a..965ba955 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -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') } diff --git a/src/pages/feed.page.tsx b/src/pages/feed.page.tsx index e05fe0f2..e695f29d 100644 --- a/src/pages/feed.page.tsx +++ b/src/pages/feed.page.tsx @@ -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 ( - + }> + + + + + + + + + ) diff --git a/src/stores/router.ts b/src/stores/router.ts index db420959..f94204fe 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -137,17 +137,16 @@ export const useRouter = = Record< const page = useStore(routerStore) const searchParams = useStore(searchParamsStore) as unknown as Accessor - const changeSearchParam = ( - key: TKey, - value: TSearchParams[TKey], - replace = false - ) => { + const changeSearchParam = (newValues: Partial, replace = false) => { const newSearchParams = { ...searchParamsStore.get() } - if (value === null) { - delete newSearchParams[key.toString()] - } else { - newSearchParams[key.toString()] = value - } + + Object.keys(newValues).forEach((key) => { + if (newValues[key] === null) { + delete newSearchParams[key.toString()] + } else { + newSearchParams[key.toString()] = newValues[key] + } + }) searchParamsStore.open(newSearchParams, replace) } diff --git a/src/stores/ui.ts b/src/stores/ui.ts index efaf8972..4bcdba76 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -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 = { + 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) }