router with createSearchParams store

This commit is contained in:
tonyrewin 2022-09-09 16:30:25 +03:00
parent 4c59293678
commit 37bfaec46e
10 changed files with 124 additions and 136 deletions

View File

@ -1,4 +1,4 @@
import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import { createEffect, createSignal, For, Show } from 'solid-js'
import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card'
import { byFirstChar, sortBy } from '../../utils/sortby'
@ -6,7 +6,7 @@ import { groupByName } from '../../utils/groupby'
import Icon from '../Nav/Icon'
import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors'
import { route, by, setBy, SortBy } from '../../stores/router'
import { route, params as paramsStore } from '../../stores/router'
import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss'
@ -18,9 +18,9 @@ export const AllAuthorsPage = (props: any) => {
const [abc, setAbc] = createSignal([])
const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || ''))
const params = useStore(paramsStore)
createEffect(() => {
if (!by() && abc().length === 0) {
if ((!params()['by'] || params()['by'] === 'abc') && abc().length === 0) {
console.log('[authors] default grouping by abc')
const grouped = { ...groupByName(authorslist()) }
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
@ -29,12 +29,10 @@ export const AllAuthorsPage = (props: any) => {
keys.sort()
setSortedKeys(keys as string[])
} else {
console.log('[authors] sorting by ' + by())
setSortedAuthors(sortBy(authorslist(), by()))
console.log('[authors] sorting by ' + params()['by'])
setSortedAuthors(sortBy(authorslist(), params()['by']))
}
}, [by()])
onMount(() => setBy('' as SortBy))
}, [authorslist(), params()])
return (
<div class="all-topics-page">
@ -50,17 +48,17 @@ export const AllAuthorsPage = (props: any) => {
<div class="row">
<div class="col">
<ul class="view-switcher">
<li classList={{ selected: by() === 'shouts' }}>
<li classList={{ selected: params()['by'] === 'shouts' }}>
<a href="/authors?by=shouts" onClick={route}>
{t('By shouts')}
</a>
</li>
<li classList={{ selected: by() === 'rating' }}>
<li classList={{ selected: params()['by'] === 'rating' }}>
<a href="/authors?by=rating" onClick={route}>
{t('By rating')}
</a>
</li>
<li classList={{ selected: !by() }}>
<li classList={{ selected: !params()['by'] || params()['by'] === 'abc' }}>
<a href="/authors" onClick={route}>
{t('By alphabet')}
</a>
@ -73,7 +71,7 @@ export const AllAuthorsPage = (props: any) => {
</li>
</ul>
<Show
when={!by()}
when={!params()['by'] || params()['by'] === 'abc'}
fallback={() => (
<div class="stats">
<For each={sortedAuthors()}>

View File

@ -1,10 +1,10 @@
import { createEffect, createSignal, For, onMount, Show } from 'solid-js'
import { createEffect, createSignal, For, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen'
import { byFirstChar, sortBy } from '../../utils/sortby'
import Icon from '../Nav/Icon'
import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics'
import { by, route, setBy } from '../../stores/router'
import { params as paramstore, route } from '../../stores/router'
import { TopicCard } from '../Topic/Card'
import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
@ -18,9 +18,9 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
const { getSortedTopics: topicslist } = useTopicsStore({ topics: props.topics })
const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || ''))
const params = useStore(paramstore)
createEffect(() => {
if (!by() && abc().length === 0) {
if (abc().length === 0 && (!params()['by'] || params()['by'] === 'abc')) {
console.log('[topics] default grouping by abc')
const grouped = { ...groupByTitle(topicslist()) }
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
@ -29,12 +29,12 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
keys.sort()
setSortedKeys(keys as string[])
} else {
console.log('[topics] sorting by ' + by())
setSortedTopics(sortBy(topicslist(), by()))
console.log('[topics] sorting by ' + params()['by'])
setSortedTopics(sortBy(topicslist(), params()['by']))
}
}, [topicslist(), by()])
}, [topicslist(), params()])
onMount(() => setBy(''))
// onMount(() => setBy(''))
return (
<>
@ -52,17 +52,17 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
<div class="row">
<div class="col">
<ul class="view-switcher">
<li classList={{ selected: by() === 'shouts' }}>
<li classList={{ selected: params()['by'] === 'shouts' }}>
<a href="/topics?by=shouts" onClick={route}>
{t('By shouts')}
</a>
</li>
<li classList={{ selected: by() === 'authors' }}>
<li classList={{ selected: params()['by'] === 'authors' }}>
<a href="/topics?by=authors" onClick={route}>
{t('By authors')}
</a>
</li>
<li classList={{ selected: !by() }}>
<li classList={{ selected: params()['by'] === 'abc' }}>
<a href="/topics" onClick={route}>
{t('By alphabet')}
</a>
@ -76,7 +76,7 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
</ul>
<Show
when={!by()}
when={params()['by'] === 'abc'}
fallback={() => (
<div class="stats">
<For each={sortedTopics()}>

View File

@ -9,8 +9,6 @@ import { slug } from '../../stores/router'
import '../../styles/Article.scss'
import '../../styles/Article.scss'
interface ArticlePageProps {
article: Shout
slug: string

View File

@ -5,11 +5,12 @@ import Row3 from '../Feed/Row3'
import AuthorFull from '../Author/Full'
import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors'
import { by, setBy } from '../../stores/router'
import { params as paramsStore } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles'
import '../../styles/Topic.scss'
import Beside from '../Feed/Beside'
import { useStore } from '@nanostores/solid'
type AuthorProps = {
authorArticles: Shout[]
@ -17,6 +18,7 @@ type AuthorProps = {
}
export const AuthorPage = (props: AuthorProps) => {
const params = useStore(paramsStore)
const { getSortedArticles: articles, getArticlesByAuthors: articlesByAuthors } = useArticlesStore({
sortedArticles: props.authorArticles
})
@ -43,13 +45,17 @@ export const AuthorPage = (props: AuthorProps) => {
}
}, [articles()])
const title = createMemo(() => {
const m = by()
const m = params()['by']
if (m === 'viewed') return t('Top viewed')
if (m === 'rating') return t('Top rated')
if (m === 'commented') return t('Top discussed')
return t('Top recent')
})
const setBy = (what: string) => {
params()['by'] = what
}
return (
<div class="container author-page">
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
@ -57,22 +63,22 @@ export const AuthorPage = (props: AuthorProps) => {
<div class="row group__controls">
<div class="col-md-8">
<ul class="view-switcher">
<li classList={{ selected: by() === '' }}>
<li classList={{ selected: !params()['by'] || params()['by'] === 'recent' }}>
<button type="button" onClick={() => setBy('')}>
{t('Recent')}
</button>
</li>
<li classList={{ selected: by() === 'rating' }}>
<li classList={{ selected: params()['by'] === 'rating' }}>
<button type="button" onClick={() => setBy('rating')}>
{t('Popular')}
</button>
</li>
<li classList={{ selected: by() === 'viewed' }}>
<li classList={{ selected: params()['by'] === 'viewed' }}>
<button type="button" onClick={() => setBy('viewed')}>
{t('Views')}
</button>
</li>
<li classList={{ selected: by() === 'commented' }}>
<li classList={{ selected: params()['by'] === 'commented' }}>
<button type="button" onClick={() => setBy('commented')}>
{t('Discussing')}
</button>

View File

@ -1,16 +1,18 @@
import '../../styles/FeedSettings.scss'
import { t } from '../../utils/intl'
import { setBy, by } from '../../stores/router' // global routing signals
import { params } from '../../stores/router' // global routing signals
import { useStore } from '@nanostores/solid'
export const FeedSettings = (props: any) => {
console.log('[feed-settings] setup articles by', by())
const args = useStore(params)
console.log('[feed-settings] setup articles by', args()['by'])
return (
<div class="container">
<h1>{t('Feed settings')}</h1>
<ul class="view-switcher">
<li class="selected">
<a href="?by=topics" onClick={() => setBy('topics')}>
<a href="?by=topics" onClick={() => (args()['by'] = 'topics')}>
{t('topics')}
</a>
</li>
@ -20,12 +22,12 @@ export const FeedSettings = (props: any) => {
</a>
</li>*/}
<li>
<a href="?by=authors" onClick={() => setBy('authors')}>
<a href="?by=authors" onClick={() => (args()['by'] = 'authors')}>
{t('authors')}
</a>
</li>
<li>
<a href="?by=reacted" onClick={() => setBy('reacted')}>
<a href="?by=reacted" onClick={() => (args()['by'] = 'reacted')}>
{t('reactions')}
</a>
</li>

View File

@ -2,16 +2,17 @@ import { Show, For, createSignal, createMemo } from 'solid-js'
import '../../styles/Search.scss'
import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from '../Feed/Card'
import { sortBy } from '../../utils/sortby'
import { t } from '../../utils/intl'
import { by, setBy } from '../../stores/router'
import { params } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles'
import { useStore } from '@nanostores/solid'
type Props = {
results: Shout[]
}
export const SearchPage = (props: Props) => {
const args = useStore(params)
const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results })
// FIXME server sort
@ -62,12 +63,12 @@ export const SearchPage = (props: Props) => {
<ul class="view-switcher">
<li class="selected">
<a href="?by=relevance" onClick={() => setBy('relevance')}>
<a href="?by=relevance" onClick={() => (args()['by'] = 'relevance')}>
{t('By relevance')}
</a>
</li>
<li>
<a href="?by=rating" onClick={() => setBy('rating')}>
<a href="?by=rating" onClick={() => (args()['by'] = 'rating')}>
{t('Top rated')}
</a>
</li>

View File

@ -7,9 +7,10 @@ import { ArticleCard } from '../Feed/Card'
import '../../styles/Topic.scss'
import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl'
import { by, setBy } from '../../stores/router'
import { params } from '../../stores/router'
import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles'
import { useStore } from '@nanostores/solid'
interface TopicProps {
topic: Topic
@ -17,6 +18,7 @@ interface TopicProps {
}
export const TopicPage = (props: TopicProps) => {
const args = useStore(params)
const { getAuthorsByTopic } = useTopicsStore({ topics: [props.topic] })
const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
const topic = createMemo(() => props.topic)
@ -43,23 +45,23 @@ export const TopicPage = (props: TopicProps) => {
<div class="row group__controls">
<div class="col-md-8">
<ul class="view-switcher">
<li classList={{ selected: !by() }}>
<button type="button" onClick={() => setBy('recent')}>
<li classList={{ selected: args()['by'] === 'recent' || !args()['by'] }}>
<button type="button" onClick={() => (args()['by'] = 'recent')}>
{t('Recent')}
</button>
</li>
<li classList={{ selected: by() === 'rating' }}>
<button type="button" onClick={() => setBy('rating')}>
<li classList={{ selected: args()['by'] === 'rating' }}>
<button type="button" onClick={() => (args()['by'] = 'rating')}>
{t('Popular')}
</button>
</li>
<li classList={{ selected: by() === 'viewed' }}>
<button type="button" onClick={() => setBy('viewed')}>
<li classList={{ selected: args()['by'] === 'viewed' }}>
<button type="button" onClick={() => (args()['by'] = 'viewed')}>
{t('Views')}
</button>
</li>
<li classList={{ selected: by() === 'commented' }}>
<button type="button" onClick={() => setBy('commented')}>
<li classList={{ selected: args()['by'] === 'commented' }}>
<button type="button" onClick={() => (args()['by'] = 'commented')}>
{t('Discussing')}
</button>
</li>

View File

@ -2,7 +2,51 @@ import { atom, action } from 'nanostores'
import type { AuthResult } from '../graphql/types.gen'
import { getLogger } from '../utils/logger'
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
import { apiClient } from '../utils/apiClient'
const log = getLogger('auth-store')
export const session = atom<AuthResult & { email: string }>()
export const session = atom<AuthResult>()
export const signIn = action(session, 'signIn', async (store, params) => {
const s = await apiClient.signIn(params)
store.set(s)
setToken(s.token)
log.debug('signed in')
})
export const signUp = action(session, 'signUp', async (store, params) => {
const s = await apiClient.signUp(params)
store.set(s)
setToken(s.token)
log.debug('signed up')
})
export const signOut = action(session, 'signOut', (store) => {
store.set(null)
resetToken()
log.debug('signed out')
})
export const emailChecks = atom<{ [email: string]: boolean }>({})
export const signCheck = action(emailChecks, 'signCheck', async (store, params) => {
store.set(await apiClient.signCheck(params))
})
export const resetCode = atom<string>()
export const signReset = action(resetCode, 'signReset', async (_store, params) => {
await apiClient.signReset(params) // { email }
resetToken()
})
export const signResend = action(resetCode, 'signResend', async (_store, params) => {
await apiClient.signResend(params) // { email }
})
export const signResetConfirm = action(session, 'signResetConfirm', async (store, params) => {
const auth = await apiClient.signResetConfirm(params) // { code }
setToken(auth.token)
store.set(auth)
})

View File

@ -1,5 +1,6 @@
import { createRouter } from '@nanostores/router'
import { createEffect, createSignal } from 'solid-js'
import { createRouter, createSearchParams } from '@nanostores/router'
import { onMount } from 'nanostores'
import { createEffect, createMemo, createSignal } from 'solid-js'
import { isServer } from 'solid-js/web'
// Types for :params in route templates
@ -18,6 +19,7 @@ interface Routes {
topic: 'slug'
}
export const params = createSearchParams()
export const router = createRouter<Routes>(
{
home: '/',
@ -40,61 +42,18 @@ export const router = createRouter<Routes>(
}
)
router.listen((r) => setResource(r.path))
const [resource, setResource] = createSignal<string>('')
const slug = createMemo<string>(() => {
const s = resource().split('/').pop()
return (Boolean(s) && router.routes.filter((x) => x[0] === s).length === 0 && s) || ''
})
// signals
const [getPage, setPage] = createSignal<number>(1)
const [getSize, setSize] = createSignal<number>(10)
export type SortBy =
| 'rating'
| 'reacted'
| 'commented'
| 'viewed'
| 'relevance'
| 'topics'
| 'authors'
| 'shouts'
| 'recent' // NOTE: not in Entity.stat
| '' // abc
const [by, setBy] = createSignal<SortBy>('')
const [slug, setSlug] = createSignal('')
const [resource, setResource] = createSignal<string>(router?.get()?.path || '')
const isSlug = (s) =>
Boolean(s) && // filter binded subroutes
router.routes.filter((x) => x[0] === s).length === 0
const encodeParams = (dict) =>
Object.entries(dict)
.map((item: [string, string]) => (item[1] ? item[0] + '=' + encodeURIComponent(item[1]) + '&' : ''))
.join('')
.slice(0, -1)
const scanParams = (href) => {
// FIXME parse query
// console.debug('[router] read url to set store', href)
// return href
// .split('?')
// .pop()
// .split('&')
// .forEach((arg: string) => {
// if (arg.startsWith('by=')) {
// setBy(arg.replace('by=', ''))
// } else if (arg.startsWith('page=')) {
// setPage(Number.parseInt(arg.replace('page=', '') || '0', 10))
// } else if (arg.startsWith('size=')) setSize(Number.parseInt(arg.replace('size=', '') || '0', 10))
// })
}
const _route = (ev) => {
const href: string = ev.target.href
console.log('[router] faster link', href)
ev.stopPropoganation()
ev.preventDefault()
router.open(href)
scanParams(href)
}
const route = (ev) => {
@ -105,37 +64,13 @@ const route = (ev) => {
}
}
const updateParams = () => {
// get request search query params
const paramsDict = {
by: by(), // sort name
page: getPage(), // page number
size: getSize() // entries per page
// TODO: add q for /search
}
console.log('[router] updated url with stored params')
return paramsDict
}
const slugDetect = () => {
const params = encodeParams(updateParams())
const route = resource() + (params ? '?' + params : '')
router.open(route) // window.pushState wrapper
const s = resource()
.split('/')
.filter((x) => x.length > 0)
.pop()
if (isSlug(s)) {
console.log('[router] detected slug {' + s + '}')
setSlug(s)
}
}
createEffect(slugDetect, [resource()])
if (!isServer) {
console.log('[router] client runtime')
createEffect(() => router.open(window.location.pathname), [window.location])
}
export { slug, route, setPage, getPage, getSize, setSize, by, setBy, resource }
onMount(router, () => {
router.listen((r) => setResource(r.path))
})
export { slug, route, resource }

View File

@ -3,7 +3,7 @@ import type { Shout } from '../../graphql/types.gen'
import type { WritableAtom } from 'nanostores'
import { useStore } from '@nanostores/solid'
import { apiClient } from '../../utils/apiClient'
import { getPage, setPage } from '../router'
import { params } from '../router'
let articleEntitiesStore: WritableAtom<Record<string, Shout>>
let sortedArticlesStore: WritableAtom<Shout[]>
@ -96,11 +96,13 @@ export const useArticlesStore = ({ sortedArticles }: InitialState) => {
}
export const loadMoreAll = () => {
setPage(getPage() + 1)
loadRecentAllArticles({ page: getPage() + 1 })
const searchParams = useStore(params)
const pn = Number.parseInt(searchParams()['page'], 10)
loadRecentAllArticles({ page: pn + 1 })
}
export const loadMorePublished = () => {
setPage(getPage() + 1)
loadRecentPublishedArticles({ page: getPage() + 1 })
const searchParams = useStore(params)
const pn = Number.parseInt(searchParams()['page'], 10)
loadRecentPublishedArticles({ page: pn + 1 })
}