Merge remote-tracking branch 'origin/dev' into navigation

# Conflicts:
#	src/components/Nav/Header.tsx
#	src/layouts/zine.astro
This commit is contained in:
Igor Lobanov 2022-09-16 12:47:00 +02:00
commit 000bfb4e45
16 changed files with 200 additions and 141 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",
"*.{scss,css}": "stylelint",
"*.{ts,tsx,js}": "eslint --fix",

View File

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

View File

@ -8,9 +8,8 @@ import { t } from '../../utils/intl'
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid'
import { session as ssession } from '../../stores/auth'
import { route, router } from '../../stores/router'
import { handleClientRouteLinkClick, router } from '../../stores/router'
import './Header.scss'
import { Shout } from '../../graphql/types.gen'
const resources = [
{ name: t('zine'), href: '/' },
@ -19,7 +18,7 @@ const resources = [
//{ name: t('community'), href: '/community' }
]
export const Header = (props: Shout) => {
export const Header = () => {
// signals
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
const [getIsScrolled, setIsScrolled] = createSignal(false)
@ -45,8 +44,21 @@ export const Header = (props: Shout) => {
// derived
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)
}
onMount(() => {
let scrollTop = window.scrollY
@ -76,11 +88,12 @@ export const Header = (props: Shout) => {
<div class="wide-container">
<nav class="row header__inner" classList={{ fixed: fixed() }}>
<div class="main-logo col-auto">
<a href="/" onClick={route}>
<a href="/" onClick={handleClientRouteLinkClick}>
<img src="/logo.svg" alt={t('Discours')} />
</a>
</div>
<div class="col main-navigation">
{/*FIXME article header*/}
<div class="article-header">
Дискурс независимый художественно-аналитический журнал с горизонтальной редакцией,
основанный на принципах свободы слова, прямой демократии и совместного редактирования.
@ -90,7 +103,7 @@ export const Header = (props: Shout) => {
<For each={resources}>
{(r: { href: string; name: string }) => (
<li classList={{ selected: r.href === subpath() }}>
<a href={r.href} onClick={route}>
<a href={r.href} onClick={handleClientRouteLinkClick}>
{r.name}
</a>
</li>
@ -101,7 +114,7 @@ export const Header = (props: Shout) => {
<div class="usernav">
<div class="usercontrol col">
<div class="usercontrol__item">
<a href="#auth" onClick={bellClick}>
<a href="#auth" onClick={handleBellIconClick}>
<div>
<Icon name="bell-white" counter={authorized() ? getWarnings().length : 1} />
</div>
@ -118,7 +131,7 @@ export const Header = (props: Shout) => {
when={authorized()}
fallback={
<div class="usercontrol__item loginbtn">
<a href="#auth" onClick={enterClick}>
<a href="#auth" onClick={handleEnterClick}>
{t('enter')}
</a>
</div>

View File

@ -16,7 +16,7 @@
flex-wrap: wrap;
justify-content: space-between;
list-style: none;
margin: 0;
margin-right: 2.2rem;
padding: 0;
}

View File

@ -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, params as paramsStore } from '../../stores/router'
import { params as paramsStore, handleClientRouteLinkClick } from '../../stores/router'
import { session } from '../../stores/auth'
import { useStore } from '@nanostores/solid'
import '../../styles/AllTopics.scss'
@ -18,7 +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 ((!params()['by'] || params()['by'] === 'abc') && abc().length === 0) {
console.log('[authors] default grouping by abc')
@ -49,17 +51,17 @@ export const AllAuthorsPage = (props: any) => {
<div class="col">
<ul class="view-switcher">
<li classList={{ selected: params()['by'] === 'shouts' }}>
<a href="/authors?by=shouts" onClick={route}>
<a href="/authors?by=shouts" onClick={handleClientRouteLinkClick}>
{t('By shouts')}
</a>
</li>
<li classList={{ selected: params()['by'] === 'rating' }}>
<a href="/authors?by=rating" onClick={route}>
<a href="/authors?by=rating" onClick={handleClientRouteLinkClick}>
{t('By rating')}
</a>
</li>
<li classList={{ selected: !params()['by'] || params()['by'] === 'abc' }}>
<a href="/authors" onClick={route}>
<a href="/authors" onClick={handleClientRouteLinkClick}>
{t('By alphabet')}
</a>
</li>

View File

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

View File

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

View File

@ -99,7 +99,7 @@ export const TopicPage = (props: TopicProps) => {
<Show when={sortedArticles().length > 5}>
<Beside
title={t('Topic is supported by')}
values={getAuthorsByTopic()[topic().slug]}
values={getAuthorsByTopic()[topic().slug].slice(0, 7)}
beside={sortedArticles()[6]}
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 { Header } from '../components/Nav/Header'
import { locale as langstore } from '../stores/ui'
import { router } from '../stores/router'
import { t } from '../utils/intl'
const { title } = Astro.params
const locale = useStore(langstore)
router.open(Astro.url.pathname) // mb doesn't work!
---
<html lang={locale() || 'ru'}>

View File

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

View File

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

View File

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

View File

@ -170,6 +170,14 @@ export const loadSearchResults = async ({
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 = {
sortedArticles?: Shout[]
topRatedArticles?: Shout[]

View File

@ -14,7 +14,9 @@ let topicEntitiesStore: WritableAtom<{ [topicSlug: string]: Topic }>
let sortedTopicsStore: ReadableAtom<Topic[]>
let topTopicsStore: ReadableAtom<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>) => {
if (topicEntitiesStore) {

View File

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