This commit is contained in:
Igor Lobanov 2022-09-14 13:28:43 +02:00
parent 55ccd419e2
commit 95f100e930
16 changed files with 199 additions and 139 deletions

View File

@ -1,5 +1,5 @@
{ {
"*.{js,ts,tsx,json,scss,css,html,astro}": "prettier --write", "*.{js,ts,tsx,json,scss,css,html}": "prettier --write",
"package.json": "sort-package-json", "package.json": "sort-package-json",
"*.{scss,css}": "stylelint", "*.{scss,css}": "stylelint",
"*.{ts,tsx,js}": "eslint --fix", "*.{ts,tsx,js}": "eslint --fix",

View File

@ -11,6 +11,7 @@ import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config' import { markdownOptions } from '../../../mdx.config'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import { incrementView, loadArticle } from '../../stores/zine/articles'
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
@ -43,14 +44,24 @@ export const FullArticle = (props: ArticleProps) => {
const auth = useStore(session) const auth = useStore(session)
onMount(() => { onMount(() => {
const b: string = props.article?.body if (!props.article.body) {
if (b?.toString().startsWith('<')) { loadArticle({ slug: props.article.slug })
setBody(b)
} else {
renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
} }
}) })
// onMount(() => {
// const b: string = props.article?.body
// if (b?.toString().startsWith('<')) {
// setBody(b)
// } else {
// renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
// }
// })
onMount(() => {
incrementView({ articleSlug: props.article.slug })
})
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
const mainTopic = () => const mainTopic = () =>

View File

@ -8,7 +8,7 @@ import { t } from '../../utils/intl'
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui' import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { session as ssession } from '../../stores/auth' import { session as ssession } from '../../stores/auth'
import { route, router } from '../../stores/router' import { handleClientRouteLinkClick, router } from '../../stores/router'
import './Header.scss' import './Header.scss'
const resources = [ const resources = [
@ -42,8 +42,22 @@ export const Header = () => {
// derived // derived
const authorized = createMemo(() => session()?.user?.slug) const authorized = createMemo(() => session()?.user?.slug)
const enterClick = route(() => showModal('auth'))
const bellClick = createMemo(() => (authorized() ? route(toggleWarnings) : enterClick)) const handleEnterClick = (ev) => {
showModal('auth')
handleClientRouteLinkClick(ev)
}
const handleBellIconClick = (ev) => {
if (!authorized()) {
handleEnterClick(ev)
return
}
toggleWarnings()
handleClientRouteLinkClick(ev)
}
return ( return (
<header> <header>
<Modal name="auth"> <Modal name="auth">
@ -52,7 +66,7 @@ export const Header = () => {
<div class="wide-container"> <div class="wide-container">
<nav class="row header__inner" classList={{ fixed: fixed() }}> <nav class="row header__inner" classList={{ fixed: fixed() }}>
<div class="main-logo col-auto"> <div class="main-logo col-auto">
<a href="/" onClick={route}> <a href="/" onClick={handleClientRouteLinkClick}>
<img src="/logo.svg" alt={t('Discours')} /> <img src="/logo.svg" alt={t('Discours')} />
</a> </a>
</div> </div>
@ -60,7 +74,7 @@ export const Header = () => {
<For each={resources}> <For each={resources}>
{(r: { href: string; name: string }) => ( {(r: { href: string; name: string }) => (
<li classList={{ selected: r.href === subpath() }}> <li classList={{ selected: r.href === subpath() }}>
<a href={r.href} onClick={route}> <a href={r.href} onClick={handleClientRouteLinkClick}>
{r.name} {r.name}
</a> </a>
</li> </li>
@ -70,7 +84,7 @@ export const Header = () => {
<div class="usernav"> <div class="usernav">
<div class="usercontrol col"> <div class="usercontrol col">
<div class="usercontrol__item"> <div class="usercontrol__item">
<a href="#auth" onClick={bellClick}> <a href="#auth" onClick={handleBellIconClick}>
<div> <div>
<Icon name="bell-white" counter={authorized() ? getWarnings().length : 1} /> <Icon name="bell-white" counter={authorized() ? getWarnings().length : 1} />
</div> </div>
@ -87,7 +101,7 @@ export const Header = () => {
when={authorized()} when={authorized()}
fallback={ fallback={
<div class="usercontrol__item loginbtn"> <div class="usercontrol__item loginbtn">
<a href="#auth" onClick={enterClick}> <a href="#auth" onClick={handleEnterClick}>
{t('enter')} {t('enter')}
</a> </a>
</div> </div>

View File

@ -11,7 +11,7 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
list-style: none; list-style: none;
margin: 0; margin-right: 2.2rem;
padding: 0; padding: 0;
} }

View File

@ -6,7 +6,7 @@ import { groupByName } from '../../utils/groupby'
import Icon from '../Nav/Icon' import Icon from '../Nav/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { route, params as paramsStore } from '../../stores/router' import { params as paramsStore, handleClientRouteLinkClick } from '../../stores/router'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss' import '../../styles/AllTopics.scss'
@ -18,7 +18,9 @@ export const AllAuthorsPage = (props: any) => {
const [abc, setAbc] = createSignal([]) const [abc, setAbc] = createSignal([])
const auth = useStore(session) const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || '')) const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || ''))
const params = useStore(paramsStore) const params = useStore(paramsStore)
createEffect(() => { createEffect(() => {
if ((!params()['by'] || params()['by'] === 'abc') && abc().length === 0) { if ((!params()['by'] || params()['by'] === 'abc') && abc().length === 0) {
console.log('[authors] default grouping by abc') console.log('[authors] default grouping by abc')
@ -49,17 +51,17 @@ export const AllAuthorsPage = (props: any) => {
<div class="col"> <div class="col">
<ul class="view-switcher"> <ul class="view-switcher">
<li classList={{ selected: params()['by'] === 'shouts' }}> <li classList={{ selected: params()['by'] === 'shouts' }}>
<a href="/authors?by=shouts" onClick={route}> <a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
{t('By shouts')} {t('By shouts')}
</a> </a>
</li> </li>
<li classList={{ selected: params()['by'] === 'rating' }}> <li classList={{ selected: params()['by'] === 'rating' }}>
<a href="/authors?by=rating" onClick={route}> <a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
{t('By rating')} {t('By rating')}
</a> </a>
</li> </li>
<li classList={{ selected: !params()['by'] || params()['by'] === 'abc' }}> <li classList={{ selected: !params()['by'] || params()['by'] === 'abc' }}>
<a href="/authors" onClick={route}> <a href="/authors" onClick={handleClientRouteLinkClick}>
{t('By alphabet')} {t('By alphabet')}
</a> </a>
</li> </li>

View File

@ -4,25 +4,30 @@ import { byFirstChar, sortBy } from '../../utils/sortby'
import Icon from '../Nav/Icon' import Icon from '../Nav/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { params as paramstore, route } from '../../stores/router' import { params as paramstore, handleClientRouteLinkClick, router } from '../../stores/router'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { groupByTitle } from '../../utils/groupby'
import '../../styles/AllTopics.scss' import '../../styles/AllTopics.scss'
import { groupByTitle } from '../../utils/groupby'
export const AllTopicsPage = (props: { topics?: Topic[] }) => { export const AllTopicsPage = (props: { topics?: Topic[] }) => {
const [sortedTopics, setSortedTopics] = createSignal<Partial<Topic>[]>([]) const [sortedTopics, setSortedTopics] = createSignal<Partial<Topic>[]>([])
const [sortedKeys, setSortedKeys] = createSignal<string[]>() const [sortedKeys, setSortedKeys] = createSignal<string[]>()
const [abc, setAbc] = createSignal([]) const [abc, setAbc] = createSignal([])
const { getSortedTopics: topicslist } = useTopicsStore({ topics: props.topics || [] }) const { getSortedTopics } = useTopicsStore({ topics: props.topics || [] })
const auth = useStore(session) const auth = useStore(session)
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || '')) const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || ''))
const params = useStore(paramstore) const params = useStore(paramstore)
console.log({ router })
createEffect(() => { createEffect(() => {
if (abc().length === 0 && (!params()['by'] || params()['by'] === 'abc')) { if (abc().length === 0 && (!params()['by'] || params()['by'] === 'abc')) {
console.log('[topics] default grouping by abc') console.log('[topics] default grouping by abc')
const grouped = { ...groupByTitle(topicslist()) } const grouped = { ...groupByTitle(getSortedTopics()) }
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar) grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
setAbc(grouped) setAbc(grouped)
const keys = Object.keys(abc) const keys = Object.keys(abc)
@ -30,88 +35,95 @@ export const AllTopicsPage = (props: { topics?: Topic[] }) => {
setSortedKeys(keys as string[]) setSortedKeys(keys as string[])
} else { } else {
console.log('[topics] sorting by ' + params()['by']) console.log('[topics] sorting by ' + params()['by'])
setSortedTopics(sortBy(topicslist(), params()['by'])) setSortedTopics(sortBy(getSortedTopics(), params()['by']))
} }
}, [topicslist(), params()]) }, [getSortedTopics(), params()])
return ( return (
<> <div class="all-topics-page">
<div class="all-topics-page"> <Show when={getSortedTopics().length > 0}>
<Show when={Boolean(sortedTopics()?.length)}> <div class="wide-container">
<div class="wide-container"> <div class="shift-content">
<div class="shift-content"> <div class="row">
<div class="row"> <div class="col-md-9 page-header">
<div class="col-md-9 page-header"> <h1>{t('Topics')}</h1>
<h1>{t('Topics')}</h1> <p>{t('Subscribe what you like to tune your personal feed')}</p>
<p>{t('Subscribe what you like to tune your personal feed')}</p>
</div>
</div> </div>
</div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<ul class="view-switcher"> <ul class="view-switcher">
<li classList={{ selected: params()['by'] === 'shouts' }}> <li classList={{ selected: params()['by'] === 'shouts' }}>
<a href="/topics?by=shouts" onClick={route}> <a href="/topics?by=shouts" onClick={handleClientRouteLinkClick}>
{t('By shouts')} {t('By shouts')}
</a> </a>
</li> </li>
<li classList={{ selected: params()['by'] === 'authors' }}> <li classList={{ selected: params()['by'] === 'authors' }}>
<a href="/topics?by=authors" onClick={route}> <a href="/topics?by=authors" onClick={handleClientRouteLinkClick}>
{t('By authors')} {t('By authors')}
</a> </a>
</li> </li>
<li classList={{ selected: params()['by'] === 'abc' }}> <li classList={{ selected: params()['by'] === 'abc' }}>
<a href="/topics" onClick={route}> <a href="/topics" onClick={handleClientRouteLinkClick}>
{t('By alphabet')} {t('By alphabet')}
</a> </a>
</li> </li>
<li class="view-switcher__search"> <li class="view-switcher__search">
<a href="/topic/search"> <a href="/topic/search">
<Icon name="search" /> <Icon name="search" />
{t('Search topic')} {t('Search topic')}
</a> </a>
</li> </li>
</ul> </ul>
<Show <div class="stats">
when={params()['by'] === 'abc'} <For each={getSortedTopics()}>
fallback={() => ( {(topic) => (
<div class="stats"> <TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
<For each={sortedTopics()}>
{(topic: Topic) => (
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
)}
</For>
</div>
)} )}
> </For>
<For each={sortedKeys() || []}>
{(letter: string) => (
<div class="group">
<h2>{letter}</h2>
<div class="container">
<div class="row">
<For each={abc()[letter]}>
{(topic: Partial<Topic>) => (
<div class="topic col-sm-6 col-md-3">
<div class="topic-title">
<a href={`/topic/${topic.slug}`}>{topic.title}</a>
</div>
</div>
)}
</For>
</div>
</div>
</div>
)}
</For>
</Show>
</div> </div>
{/*FIXME*/}
{/*<Show*/}
{/* when={params()['by'] === 'abc'}*/}
{/* fallback={() => (*/}
{/* <div class="stats">*/}
{/* <For each={getSortedTopics()}>*/}
{/* {(topic: Topic) => (*/}
{/* <TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />*/}
{/* )}*/}
{/* </For>*/}
{/* </div>*/}
{/* )}*/}
{/*>*/}
{/* <For each={sortedKeys() || []}>*/}
{/* {(letter: string) => (*/}
{/* <div class="group">*/}
{/* <h2>{letter}</h2>*/}
{/* <div class="container">*/}
{/* <div class="row">*/}
{/* <For each={abc()[letter]}>*/}
{/* {(topic: Partial<Topic>) => (*/}
{/* <div class="topic col-sm-6 col-md-3">*/}
{/* <div class="topic-title">*/}
{/* <a href={`/topic/${topic.slug}`}>{topic.title}</a>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </For>*/}
{/* </div>*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/* </For>*/}
{/*</Show>*/}
</div> </div>
</div> </div>
</div> </div>
</Show> </div>
</div> </Show>
</> </div>
) )
} }

View File

@ -16,6 +16,7 @@ import { t } from '../../utils/intl'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles' import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { router } from '../../stores/router'
type HomeProps = { type HomeProps = {
randomTopics: Topic[] randomTopics: Topic[]
@ -26,7 +27,7 @@ type HomeProps = {
offset?: number offset?: number
} }
const LAYOUTS = ['article', 'prose', 'music', 'video', 'image'] // const LAYOUTS = ['article', 'prose', 'music', 'video', 'image']
export const HomePage = (props: HomeProps) => { export const HomePage = (props: HomeProps) => {
const [someLayout, setSomeLayout] = createSignal([] as Shout[]) const [someLayout, setSomeLayout] = createSignal([] as Shout[])

View File

@ -99,7 +99,7 @@ export const TopicPage = (props: TopicProps) => {
<Show when={sortedArticles().length > 5}> <Show when={sortedArticles().length > 5}>
<Beside <Beside
title={t('Topic is supported by')} title={t('Topic is supported by')}
values={getAuthorsByTopic()[topic().slug]} values={getAuthorsByTopic()[topic().slug].slice(0, 7)}
beside={sortedArticles()[6]} beside={sortedArticles()[6]}
wrapper={'author'} wrapper={'author'}
/> />

View File

@ -0,0 +1,15 @@
import { isServer } from 'solid-js/web'
import { router } from '../../stores/router'
type Props = {
href: string
children: any
}
export const ServerRouterProvider = (props: Props) => {
if (isServer) {
router.open(props.href)
}
return props.children
}

View File

@ -4,12 +4,10 @@ import { useStore } from '@nanostores/solid'
import { Suspense } from 'solid-js' import { Suspense } from 'solid-js'
import { Header } from '../components/Nav/Header' import { Header } from '../components/Nav/Header'
import { locale as langstore } from '../stores/ui' import { locale as langstore } from '../stores/ui'
import { router } from '../stores/router'
import { t } from '../utils/intl' import { t } from '../utils/intl'
const { title } = Astro.params const { title } = Astro.params
const locale = useStore(langstore) const locale = useStore(langstore)
router.open(Astro.url.pathname) // mb doesn't work!
--- ---
<html lang={locale() || 'ru'}> <html lang={locale() || 'ru'}>

View File

@ -3,30 +3,33 @@ import '../styles/app.scss'
import { Suspense } from 'solid-js' import { Suspense } from 'solid-js'
import { Header } from '../components/Nav/Header' import { Header } from '../components/Nav/Header'
import { Footer } from '../components/Discours/Footer' import { Footer } from '../components/Discours/Footer'
import { ServerRouterProvider } from '../components/providers/ServerRouterProvider'
import { t } from '../utils/intl' import { t } from '../utils/intl'
import { locale as langstore } from '../stores/ui' import { locale as langstore } from '../stores/ui'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { router } from '../stores/router'
const { pathname, search } = Astro.url
// FIXME always returns ru
const locale = useStore(langstore) const locale = useStore(langstore)
// FIXME why
router.open(Astro.url.pathname) // looks like doesn't work!
--- ---
<html lang={locale() || 'en'}> <ServerRouterProvider href={pathname + search}>
<head> <html lang={locale() || 'en'}>
<meta charset="utf-8" /> <head>
<meta name="viewport" content="width=device-width" /> <meta charset="utf-8" />
<link rel="icon" type="image/png" href="/favicon.png" /> <meta name="viewport" content="width=device-width" />
<title>{t('Discours')}</title> <link rel="icon" type="image/png" href="/favicon.png" />
</head> <title>{t('Discours')}</title>
<body> </head>
<Header /> <body>
<main class="main-content"> <Header />
<Suspense> <main class="main-content">
<slot /> <Suspense>
</Suspense> <slot />
</main> </Suspense>
<Footer /> </main>
</body> <Footer />
</html> </body>
</html>
</ServerRouterProvider>

View File

@ -15,8 +15,8 @@ Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate'
<Zine> <Zine>
<HomePage <HomePage
randomTopics={randomTopics}
recentPublishedArticles={recentPublished} recentPublishedArticles={recentPublished}
randomTopics={randomTopics}
topMonthArticles={topMonth} topMonthArticles={topMonth}
topOverallArticles={topOverall} topOverallArticles={topOverall}
client:load client:load

View File

@ -1,5 +1,4 @@
import { createRouter, createSearchParams } from '@nanostores/router' import { createRouter, createSearchParams } from '@nanostores/router'
import { createEffect } from 'solid-js'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
// Types for :params in route templates // Types for :params in route templates
@ -41,24 +40,15 @@ export const router = createRouter<Routes>(
} }
) )
// suppresses reload export const handleClientRouteLinkClick = (ev) => {
export const route = (ev) => { const href = ev.target.href
// eslint-disable-next-line unicorn/consistent-function-scoping console.log('[router] faster link', href)
const _route = (ev) => { ev.stopPropagation()
const href: string = ev.target.href ev.preventDefault()
console.log('[router] faster link', href) router.open(href)
ev.stopPropoganation()
ev.preventDefault()
router.open(href)
}
if (typeof ev === 'function') {
return _route
} else if (!isServer && ev?.target && ev.target.href) {
_route(ev)
}
} }
if (!isServer) { if (!isServer) {
console.log('[router] client runtime') const { pathname, search } = window.location
createEffect(() => router.open(window.location.pathname), [window.location]) router.open(pathname + search)
} }

View File

@ -170,6 +170,14 @@ export const loadSearchResults = async ({
addSortedArticles(newArticles) addSortedArticles(newArticles)
} }
export const incrementView = async ({ articleSlug }: { articleSlug: string }): Promise<void> => {
await apiClient.incrementView({ articleSlug })
}
export const loadArticle = async ({ slug }: { slug: string }): Promise<Shout> => {
return await apiClient.getArticle({ slug })
}
type InitialState = { type InitialState = {
sortedArticles?: Shout[] sortedArticles?: Shout[]
topRatedArticles?: Shout[] topRatedArticles?: Shout[]

View File

@ -14,7 +14,9 @@ let topicEntitiesStore: WritableAtom<{ [topicSlug: string]: Topic }>
let sortedTopicsStore: ReadableAtom<Topic[]> let sortedTopicsStore: ReadableAtom<Topic[]>
let topTopicsStore: ReadableAtom<Topic[]> let topTopicsStore: ReadableAtom<Topic[]>
let randomTopicsStore: WritableAtom<Topic[]> let randomTopicsStore: WritableAtom<Topic[]>
let topicsByAuthorStore: WritableAtom<{ [authorSlug: string]: Topic[] }> let topicsByAuthorStore: WritableAtom<{ [authorSlug: string]: Topic[] }> = atom<{
[authorSlug: string]: Topic[]
}>({})
const initStore = (initial?: Record<string, Topic>) => { const initStore = (initial?: Record<string, Topic>) => {
if (topicEntitiesStore) { if (topicEntitiesStore) {

View File

@ -30,6 +30,7 @@ import authReset from '../graphql/mutation/auth-reset'
import authForget from '../graphql/mutation/auth-forget' import authForget from '../graphql/mutation/auth-forget'
import authResend from '../graphql/mutation/auth-resend' import authResend from '../graphql/mutation/auth-resend'
import authorsBySlugs from '../graphql/query/authors-by-slugs' import authorsBySlugs from '../graphql/query/authors-by-slugs'
import incrementView from '../graphql/mutation/increment-view'
const log = getLogger('api-client') const log = getLogger('api-client')
@ -81,7 +82,7 @@ export const apiClient = {
}) })
.toPromise() .toPromise()
return response.data.searchQuery return response.data?.searchQuery || []
}, },
getRecentArticles: async ({ getRecentArticles: async ({
limit = FEED_SIZE, limit = FEED_SIZE,
@ -266,5 +267,8 @@ export const apiClient = {
const response = await privateGraphQLClient.mutation(reactionDestroy, { id }).toPromise() const response = await privateGraphQLClient.mutation(reactionDestroy, { id }).toPromise()
return response.data.deleteReaction return response.data.deleteReaction
},
incrementView: async ({ articleSlug }) => {
await privateGraphQLClient.mutation(incrementView, { shout: articleSlug })
} }
} }