Merge pull request #1 from Discours/store-layer

store layer
This commit is contained in:
Tony 2022-09-13 17:20:05 +03:00 committed by GitHub
commit eaad1a1307
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 730 additions and 560 deletions

View File

@ -3,3 +3,5 @@ public
*.cjs *.cjs
src/graphql/*.gen.ts src/graphql/*.gen.ts
src/legacy_* src/legacy_*
dist/
.vercel/

2
.stylelintignore Normal file
View File

@ -0,0 +1,2 @@
.vercel/
dist/

View File

@ -9,6 +9,7 @@ import { t } from '../../utils/intl'
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions' // import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
import { renderMarkdown } from '@astrojs/markdown-remark' import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config' import { markdownOptions } from '../../../mdx.config'
import { deleteReaction } from '../../stores/zine/reactions'
export default (props: { export default (props: {
level?: number level?: number
@ -20,16 +21,16 @@ export default (props: {
const [body, setBody] = createSignal('') const [body, setBody] = createSignal('')
onMount(() => { onMount(() => {
const b: string = props.comment?.body const b: string = props.comment?.body
if (b?.toString().startsWith('<')) setBody(b) if (b?.toString().startsWith('<')) {
else { setBody(b)
} else {
renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code)) renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
} }
}) })
const remove = () => { const remove = () => {
if (comment()?.id) { if (comment()?.id) {
console.log('[comment] removing', comment().id) console.log('[comment] removing', comment().id)
// FIXME deleteReaction(comment().id)
// deleteReaction(comment().id)
} }
} }
@ -79,7 +80,7 @@ export default (props: {
</button> </button>
<Show when={props.canEdit}> <Show when={props.canEdit}>
{/*FIXME*/} {/*FIXME implement edit comment modal*/}
{/*<button*/} {/*<button*/}
{/* class="comment-control comment-control--edit"*/} {/* class="comment-control comment-control--edit"*/}
{/* onClick={() => showModal('editComment')}*/} {/* onClick={() => showModal('editComment')}*/}
@ -93,7 +94,7 @@ export default (props: {
</button> </button>
</Show> </Show>
{/*FIXME*/} {/*FIXME implement modals */}
{/*<button*/} {/*<button*/}
{/* class="comment-control comment-control--share"*/} {/* class="comment-control comment-control--share"*/}
{/* onClick={() => showModal('shareComment')}*/} {/* onClick={() => showModal('shareComment')}*/}

View File

@ -9,6 +9,9 @@ import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { renderMarkdown } from '@astrojs/markdown-remark' import { renderMarkdown } from '@astrojs/markdown-remark'
import { markdownOptions } from '../../../mdx.config' import { markdownOptions } from '../../../mdx.config'
import { useStore } from '@nanostores/solid'
import { session } from '../../stores/auth'
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
const getCommentLevel = (comment: Reaction, level = 0) => { const getCommentLevel = (comment: Reaction, level = 0) => {
@ -36,10 +39,14 @@ const formatDate = (date: Date) => {
export const FullArticle = (props: ArticleProps) => { export const FullArticle = (props: ArticleProps) => {
const [body, setBody] = createSignal('') const [body, setBody] = createSignal('')
const auth = useStore(session)
onMount(() => { onMount(() => {
const b: string = props.article?.body const b: string = props.article?.body
if (b?.toString().startsWith('<')) setBody(b) if (b?.toString().startsWith('<')) {
else { setBody(b)
} else {
renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code)) renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
} }
}) })
@ -136,16 +143,15 @@ export const FullArticle = (props: ArticleProps) => {
<div class="shout-stats__item shout-stats__item--date">{formattedDate}</div> <div class="shout-stats__item shout-stats__item--date">{formattedDate}</div>
</div> </div>
{/*FIXME*/} <div class="topics-list">
{/*<div class="topics-list">*/} <For each={props.article.topics}>
{/* <For each={props.article.topics}>*/} {(topic) => (
{/* {(topic) => (*/} <div class="shout__topic">
{/* <div class="shout__topic">*/} <a href={`/topic/${topic.slug}`}>{topic.title}</a>
{/* <a href={`/topic/${topic.slug}`}>{props.topicsBySlug[topic.slug].title}</a>*/} </div>
{/* </div>*/} )}
{/* )}*/} </For>
{/* </For>*/} </div>
{/*</div>*/}
<div class="shout__authors-list"> <div class="shout__authors-list">
<Show when={props.article?.authors?.length > 1}> <Show when={props.article?.authors?.length > 1}>
@ -166,32 +172,28 @@ export const FullArticle = (props: ArticleProps) => {
<ArticleComment <ArticleComment
comment={reaction} comment={reaction}
level={getCommentLevel(reaction)} level={getCommentLevel(reaction)}
// FIXME canEdit={reaction.createdBy?.slug === auth()?.user?.slug}
// canEdit={reaction.createdBy?.slug === session()?.user?.slug}
canEdit={false}
/> />
)} )}
</For> </For>
</Show> </Show>
<Show when={!auth()?.user?.slug}>
{/*FIXME*/} <div class="comment-warning" id="comments">
{/*<Show when={!session()?.user?.slug}>*/} {t('To leave a comment you please')}
{/* <div class="comment-warning" id="comments">*/} <a
{/* {t('To leave a comment you please')}*/} href={''}
{/* <a*/} onClick={(evt) => {
{/* href={''}*/} evt.preventDefault()
{/* onClick={(evt) => {*/} showModal('auth')
{/* evt.preventDefault()*/} }}
{/* showModal('auth')*/} >
{/* }}*/} <i>{t('sign up or sign in')}</i>
{/* >*/} </a>
{/* <i>{t('sign up or sign in')}</i>*/} </div>
{/* </a>*/} </Show>
{/* </div>*/} <Show when={auth()?.user?.slug}>
{/*</Show>*/} <textarea class="write-comment" rows="1" placeholder={t('Write comment')} />
{/*<Show when={session()?.user?.slug}>*/} </Show>
{/* <textarea class="write-comment" rows="1" placeholder={t('Write comment')} />*/}
{/*</Show>*/}
</div> </div>
</div> </div>
) )

View File

@ -21,7 +21,6 @@ interface AuthorCardProps {
} }
export const AuthorCard = (props: AuthorCardProps) => { export const AuthorCard = (props: AuthorCardProps) => {
// const [zine, { follow, unfollow }] = useZine()
const locale = useStore(locstore) const locale = useStore(locstore)
const auth = useStore(session) const auth = useStore(session)
const subscribed = createMemo( const subscribed = createMemo(

View File

@ -24,7 +24,6 @@ const Link = (
<button <button
class={clsx('sidebar-link', props.className)} class={clsx('sidebar-link', props.className)}
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }} style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
// eslint-disable-next-line solid/reactivity
onClick={props.onClick} onClick={props.onClick}
disabled={props.disabled} disabled={props.disabled}
title={props.title} title={props.title}
@ -34,9 +33,52 @@ const Link = (
</button> </button>
) )
// FIXME type FileLinkProps = {
// eslint-disable-next-line sonarjs/cognitive-complexity file: File
export default () => { onOpenFile: (file: File) => void
}
const FileLink = (props: FileLinkProps) => {
const length = 100
let content = ''
const getContent = (node: any) => {
if (node.text) {
content += node.text
}
if (content.length > length) {
content = `${content.slice(0, Math.max(0, length))}...`
return content
}
if (node.content) {
for (const child of node.content) {
if (content.length >= length) {
break
}
content = getContent(child)
}
}
return content
}
const text = () =>
props.file.path
? props.file.path.slice(Math.max(0, props.file.path.length - length))
: getContent(props.file.text?.doc)
return (
// eslint-disable-next-line solid/no-react-specific-props
<Link className="file" onClick={() => props.onOpenFile(props.file)} data-testid="open">
{text()} {props.file.path && '📎'}
</Link>
)
}
export const Sidebar = () => {
const [store, ctrl] = useState() const [store, ctrl] = useState()
const [lastAction, setLastAction] = createSignal<string | undefined>() const [lastAction, setLastAction] = createSignal<string | undefined>()
const toggleTheme = () => { const toggleTheme = () => {
@ -102,46 +144,6 @@ export default () => {
// }) // })
// } // }
const FileLink = (p: { file: File }) => {
const length = 100
let content = ''
const getContent = (node: any) => {
if (node.text) {
content += node.text
}
if (content.length > length) {
content = `${content.slice(0, Math.max(0, length))}...`
return content
}
if (node.content) {
for (const child of node.content) {
if (content.length >= length) {
break
}
content = getContent(child)
}
}
return content
}
const text = () =>
p.file.path
? p.file.path.slice(Math.max(0, p.file.path.length - length))
: getContent(p.file.text?.doc)
return (
// eslint-disable-next-line solid/no-react-specific-props
<Link className="file" onClick={() => onOpenFile(p.file)} data-testid="open">
{text()} {p.file.path && '📎'}
</Link>
)
}
// const Keys = (props: { keys: string[] }) => ( // const Keys = (props: { keys: string[] }) => (
// <span> // <span>
// <For each={props.keys}>{(k: string) => <i>{k}</i>}</For> // <For each={props.keys}>{(k: string) => <i>{k}</i>}</For>
@ -229,7 +231,7 @@ export default () => {
<Show when={store.files?.length > 0}> <Show when={store.files?.length > 0}>
<h4>Files:</h4> <h4>Files:</h4>
<p> <p>
<For each={store.files}>{(file) => <FileLink file={file} />}</For> <For each={store.files}>{(file) => <FileLink file={file} onOpenFile={onOpenFile} />}</For>
</p> </p>
</Show> </Show>

View File

@ -1,30 +1,32 @@
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import type { Author, Shout } from '../../graphql/types.gen' import { For } from 'solid-js'
import type { Author } from '../../graphql/types.gen'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import Icon from '../Nav/Icon' import Icon from '../Nav/Icon'
import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles'
import { useSeenStore } from '../../stores/zine/seen'
type FeedSidebarProps = { type FeedSidebarProps = {
authors: Author[] authors: Author[]
} }
export const FeedSidebar = (props: FeedSidebarProps) => { export const FeedSidebar = (props: FeedSidebarProps) => {
// const seen = useStore(seenstore) const { getSeen: seen } = useSeenStore()
const auth = useStore(session) const auth = useStore(session)
// const topics = useTopicsStore() const { getSortedAuthors: authors } = useAuthorsStore({ authors: props.authors })
const { getSortedAuthors: authors } = useAuthorsStore() const { getArticlesByTopic } = useArticlesStore()
// const articlesByTopics = useStore(abt) const { getTopicEntities } = useTopicsStore()
// const topicIsSeen = (topic: string) => { const checkTopicIsSeen = (topicSlug: string) => {
// let allSeen = false return getArticlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug]))
// articlesByTopics()[topic].forEach((s: Shout) => (allSeen = !seen()[s.slug])) }
// return allSeen
// } const checkAuthorIsSeen = (authorSlug: string) => {
// return Boolean(seen()[authorSlug])
// const authorIsSeen = (slug: string) => { }
// return !!seen()[slug]
// }
return ( return (
<> <>
@ -61,27 +63,27 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
<strong>{t('My subscriptions')}</strong> <strong>{t('My subscriptions')}</strong>
</a> </a>
</li> </li>
{/*FIXME rework seen*/}
{/*<For each={auth()?.info?.authors}>*/}
{/* {(aslug) => (*/}
{/* <li>*/}
{/* <a href={`/author/${aslug}`} classList={{ unread: authorIsSeen(aslug) }}>*/}
{/* <small>@{aslug}</small>*/}
{/* {(authors()[aslug] as Author).name}*/}
{/* </a>*/}
{/* </li>*/}
{/* )}*/}
{/*</For>*/}
{/*<For each={auth()?.info?.topics as string[]}>*/} <For each={auth()?.info?.authors}>
{/* {(topic: string) => (*/} {(authorSlug) => (
{/* <li>*/} <li>
{/* <a href={`/author/${topic}`} classList={{ unread: topicIsSeen(topic) }}>*/} <a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}>
{/* {topics()[topic]?.title}*/} <small>@{authorSlug}</small>
{/* </a>*/} {authors()[authorSlug].name}
{/* </li>*/} </a>
{/* )}*/} </li>
{/*</For>*/} )}
</For>
<For each={auth()?.info?.topics}>
{(topicSlug) => (
<li>
<a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}>
{getTopicEntities()[topicSlug]?.title}
</a>
</li>
)}
</For>
</ul> </ul>
<p class="settings"> <p class="settings">

View File

@ -6,7 +6,7 @@ import { Form } from 'solid-js-form'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { hideModal, useModalStore } from '../../stores/ui' import { hideModal, useModalStore } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { session as sessionstore, signIn, renewSession } from '../../stores/auth' import { session as sessionstore, signIn } from '../../stores/auth'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { useValidator } from '../../utils/validators' import { useValidator } from '../../utils/validators'

View File

@ -3,10 +3,12 @@ import { Show } from 'solid-js/web'
import './Card.scss' import './Card.scss'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { locale as locstore } from '../../stores/ui' import { locale as locstore } from '../../stores/ui'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import { follow, unfollow } from '../../stores/zine/common'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -22,22 +24,18 @@ export const TopicCard = (props: TopicProps) => {
const topic = createMemo(() => props.topic) const topic = createMemo(() => props.topic)
// const subscribed = createMemo(() => { const subscribed = createMemo(() => {
// return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false
// }) })
// FIXME use store actions // FIXME use store actions
// const subscribe = async (really = true) => { const subscribe = async (really = true) => {
// if (really) { if (really) {
// const result = await apiClient.q(follow, { what: 'topic', slug: topic().slug }) follow({ what: FollowingEntity.Topic, slug: topic().slug })
// if (result.error) console.error(result.error) } else {
// // TODO: setSubscribers(topic().stat?.followers as number + 1) unfollow({ what: FollowingEntity.Topic, slug: topic().slug })
// } else { }
// const result = await apiClient.q(unfollow, { what: 'topic', slug: topic().slug }) }
// if (result.error) console.error(result.error)
// // TODO: setSubscribers(topic().stat?.followers as number - 1)
// }
// }
return ( return (
<div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}> <div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}>
<div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}> <div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}>
@ -101,19 +99,18 @@ export const TopicCard = (props: TopicProps) => {
</Show> </Show>
</div> </div>
<div classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }}> <div classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }}>
{/*FIXME*/} <Show
{/*<Show*/} when={subscribed()}
{/* when={subscribed()}*/} fallback={
{/* fallback={*/} <button onClick={() => subscribe(true)} class="button--light">
{/* <button onClick={() => subscribe(true)} class="button--light">*/} +&nbsp;{t('Follow')}
{/* +&nbsp;{t('Follow')}*/} </button>
{/* </button>*/} }
{/* }*/} >
{/*>*/} <button onClick={() => subscribe(false)} class="button--light">
{/* <button onClick={() => subscribe(false)} class="button--light">*/} -&nbsp;{t('Unfollow')}
{/* -&nbsp;{t('Unfollow')}*/} </button>
{/* </button>*/} </Show>
{/*</Show>*/}
</div> </div>
</div> </div>
) )

View File

@ -18,7 +18,7 @@ interface ArticlePageProps {
const ARTICLE_COMMENTS_PAGE_SIZE = 50 const ARTICLE_COMMENTS_PAGE_SIZE = 50
export const ArticlePage = (props: ArticlePageProps) => { export const ArticlePage = (props: ArticlePageProps) => {
const { getCurrentArticle } = useCurrentArticleStore(props.article) const { getCurrentArticle } = useCurrentArticleStore({ currentArticle: props.article })
const [getCommentsPage] = createSignal(1) const [getCommentsPage] = createSignal(1)
const [getIsCommentsLoading, setIsCommentsLoading] = createSignal(false) const [getIsCommentsLoading, setIsCommentsLoading] = createSignal(false)

View File

@ -1,7 +1,8 @@
import { Show, createMemo } from 'solid-js' import { Show, createMemo } from 'solid-js'
import type { Author, Shout, Topic } from '../../graphql/types.gen' import type { Author, Reaction, Shout } from '../../graphql/types.gen'
import Row2 from '../Feed/Row2' import Row2 from '../Feed/Row2'
import Row3 from '../Feed/Row3' import Row3 from '../Feed/Row3'
import Beside from '../Feed/Beside'
import AuthorFull from '../Author/Full' import AuthorFull from '../Author/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
@ -9,31 +10,35 @@ import { params } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles' import { useArticlesStore } from '../../stores/zine/articles'
import '../../styles/Topic.scss' import '../../styles/Topic.scss'
import Beside from '../Feed/Beside'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { unique } from '../../utils'
// TODO: load reactions on client
type AuthorProps = { type AuthorProps = {
authorArticles: Shout[] authorArticles: Shout[]
author: Author author: Author
// FIXME author topics fro server
// topics: Topic[]
} }
export const AuthorPage = (props: AuthorProps) => { export const AuthorPage = (props: AuthorProps) => {
const args = useStore(params) const { getSortedArticles: articles } = useArticlesStore({
const { getSortedArticles: articles, getArticlesByAuthors: articlesByAuthors } = useArticlesStore({
sortedArticles: props.authorArticles sortedArticles: props.authorArticles
}) })
const { getAuthorEntities: authors } = useAuthorsStore([props.author]) const { getAuthorEntities: authors } = useAuthorsStore({ authors: [props.author] })
const { getTopicsByAuthor } = useTopicsStore()
const author = createMemo(() => authors()[props.author.slug]) const author = createMemo(() => authors()[props.author.slug])
const topics = createMemo(() => { const args = useStore(params)
const ttt = []
articlesByAuthors()[author().slug].forEach((s: Shout) => //const slug = createMemo(() => author().slug)
s.topics.forEach((tpc: Topic) => ttt.push(tpc)) /*
) const slug = createMemo<string>(() => {
return unique(ttt) let slug = props?.slug
if (props?.slug.startsWith('@')) slug = slug.replace('@', '')
return slug
}) })
const { getSortedTopics } = useTopicsStore({ topics: topics() }) */
const title = createMemo(() => { const title = createMemo(() => {
const m = args()['by'] const m = args()['by']
@ -86,7 +91,7 @@ export const AuthorPage = (props: AuthorProps) => {
<Show when={articles()?.length > 0}> <Show when={articles()?.length > 0}>
<Beside <Beside
title={t('Topics which supported by author')} title={t('Topics which supported by author')}
values={getSortedTopics()?.slice(0, 5)} values={getTopicsByAuthor()[author().slug].slice(0, 5)}
beside={articles()[0]} beside={articles()[0]}
wrapper={'topic'} wrapper={'topic'}
topicShortDescription={true} topicShortDescription={true}

View File

@ -4,7 +4,7 @@ import { State, StateContext } from '../Editor/prosemirror/context'
import { createCtrl } from '../Editor/store/ctrl' import { createCtrl } from '../Editor/store/ctrl'
import { Layout } from '../Editor/Layout' import { Layout } from '../Editor/Layout'
import Editor from '../Editor' import Editor from '../Editor'
import Sidebar from '../Editor/Sidebar' import { Sidebar } from '../Editor/Sidebar'
import ErrorView from '../Editor/Error' import ErrorView from '../Editor/Error'
import { newState } from '../Editor/store' import { newState } from '../Editor/store'

View File

@ -1,68 +1,64 @@
import { createMemo, For, Show } from 'solid-js' import { createMemo, For, Show } from 'solid-js'
import { Shout, Reaction, ReactionKind, Topic, Author } from '../../graphql/types.gen' import type { Shout, Reaction } from '../../graphql/types.gen'
import '../../styles/Feed.scss' import '../../styles/Feed.scss'
import Icon from '../Nav/Icon' import Icon from '../Nav/Icon'
import { byCreated, sortBy } from '../../utils/sortby' import { byCreated, sortBy } from '../../utils/sortby'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { FeedSidebar } from '../Feed/Sidebar'
import { session } from '../../stores/auth' import { session } from '../../stores/auth'
import CommentCard from '../Article/Comment' import CommentCard from '../Article/Comment'
import { loadMoreAll, useArticlesStore } from '../../stores/zine/articles' import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { FeedSidebar } from '../Feed/Sidebar'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { unique } from '../../utils'
import { AuthorCard } from '../Author/Card'
interface FeedProps { interface FeedProps {
recentArticles: Shout[] articles: Shout[]
reactions: Reaction[] reactions: Reaction[]
page?: number
size?: number
} }
const AUTHORSHIP_REACTIONS = [ // const AUTHORSHIP_REACTIONS = [
ReactionKind.Accept, // ReactionKind.Accept,
ReactionKind.Reject, // ReactionKind.Reject,
ReactionKind.Propose, // ReactionKind.Propose,
ReactionKind.Ask // ReactionKind.Ask
] // ]
export const FeedPage = (props: FeedProps) => { export const FeedPage = (props: FeedProps) => {
// state // state
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.recentArticles }) const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles })
const reactions = useReactionsStore(props.reactions) const reactions = useReactionsStore(props.reactions)
const { const { getTopAuthors, getSortedAuthors: authors } = useAuthorsStore()
// getAuthorEntities: authorsBySlug, const { getTopTopics } = useTopicsStore()
getSortedAuthors: authors
} = useAuthorsStore() // test if it catches preloaded authors
const auth = useStore(session) const auth = useStore(session)
const topics = createMemo(() => {
const ttt = []
articles().forEach((s: Shout) => s.topics.forEach((tpc: Topic) => ttt.push(tpc)))
return unique(ttt)
})
const { getSortedTopics } = useTopicsStore({ topics: topics() })
// derived
const topReactions = createMemo(() => sortBy(reactions(), byCreated)) const topReactions = createMemo(() => sortBy(reactions(), byCreated))
const topAuthors = createMemo(() => sortBy(authors(), 'shouts'))
// note this became synthetic
// methods // const expectingFocus = createMemo<Shout[]>(() => {
// // 1 co-author notifications needs
// // TODO: list of articles where you are co-author
// // TODO: preload proposals
// // TODO: (maybe?) and changes history
// console.debug(reactions().filter((r) => r.kind in AUTHORSHIP_REACTIONS))
//
// // 2 community self-regulating mechanics
// // TODO: query all new posts to be rated for publishing
// // TODO: query all reactions where user is in authors list
// return []
// })
const expectingFocus = createMemo<Shout[]>(() => { const loadMore = () => {
// 1 co-author notifications needs // FIXME
// TODO: list of articles where you are co-author const page = (props.page || 1) + 1
// TODO: preload proposals loadRecentArticles({ page })
// TODO: (maybe?) and changes history }
console.debug(reactions().filter((r) => r.kind in AUTHORSHIP_REACTIONS))
// 2 community self-regulating mechanics
// TODO: query all new posts to be rated for publishing
// TODO: query all reactions where user is in authors list
return []
})
return ( return (
<> <>
<div class="container feed"> <div class="container feed">
@ -103,8 +99,8 @@ export const FeedPage = (props: FeedProps) => {
</div> </div>
<ul class="beside-column"> <ul class="beside-column">
<For each={topAuthors()}> <For each={getTopAuthors().slice(0, 5)}>
{(author: Author) => ( {(author) => (
<li> <li>
<AuthorCard author={author} compact={true} hasLink={true} /> <AuthorCard author={author} compact={true} hasLink={true} />
</li> </li>
@ -125,22 +121,23 @@ export const FeedPage = (props: FeedProps) => {
<aside class="col-md-3"> <aside class="col-md-3">
<section class="feed-comments"> <section class="feed-comments">
<h4>{t('Comments')}</h4> <h4>{t('Comments')}</h4>
<For each={topReactions() as Reaction[]}> <For each={topReactions()}>
{(c: Reaction) => <CommentCard comment={c} compact={true} />} {(comment) => <CommentCard comment={comment} compact={true} />}
</For> </For>
</section> </section>
<Show when={getSortedTopics().length > 0}> <Show when={getTopTopics().length > 0}>
<section class="feed-topics"> <section class="feed-topics">
<h4>{t('Topics')}</h4> <h4>{t('Topics')}</h4>
<For each={getSortedTopics().slice(0, 5)}> <For each={getTopTopics().slice(0, 5)}>
{(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />} {(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />}
</For> </For>
</section> </section>
</Show> </Show>
</aside> </aside>
</div> </div>
<p class="load-more-container"> <p class="load-more-container">
<button class="button" onClick={loadMoreAll}> <button class="button" onClick={loadMore}>
{t('Load more')} {t('Load more')}
</button> </button>
</p> </p>

View File

@ -13,19 +13,9 @@ import Group from '../Feed/Group'
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import Icon from '../Nav/Icon' import Icon from '../Nav/Icon'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import {
setTopRated,
topRated,
topViewed,
topAuthors,
topTopics,
topRatedMonth,
topCommented
} from '../../stores/zine/top'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { loadMorePublished, useArticlesStore } from '../../stores/zine/articles' import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
import { sortBy } from '../../utils/sortby' import { useAuthorsStore } from '../../stores/zine/authors'
import { shuffle } from '../../utils'
type HomeProps = { type HomeProps = {
randomTopics: Topic[] randomTopics: Topic[]
@ -41,72 +31,98 @@ 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[])
const [selectedLayout, setSelectedLayout] = createSignal('article') const [selectedLayout, setSelectedLayout] = createSignal('article')
const [byLayout, setByLayout] = createSignal<{ [layout: string]: Shout[] }>({}) const [byLayout, setByLayout] = createSignal({} as { [layout: string]: Shout[] })
const { getSortedArticles: articles, getArticlesByTopics: byTopic } = useArticlesStore({ const [byTopic, setByTopic] = createSignal({} as { [topic: string]: Shout[] })
sortedArticles: props.recentPublishedArticles
})
const { getSortedTopics } = useTopicsStore({ topics: sortBy(props.randomTopics, 'shouts') })
createEffect(() => { const {
if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) { getSortedArticles,
console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded') getTopRatedArticles,
console.log('[home] preparing published articles...') getTopRatedMonthArticles,
// get shouts lists by getTopViewedArticles,
const bl: { [key: string]: Shout[] } = {} getTopCommentedArticles
articles().forEach((s: Shout) => { } = useArticlesStore({
// by layout sortedArticles: props.recentPublishedArticles,
const l = s.layout || 'article' topRatedArticles: props.topOverallArticles,
if (!bl[l]) bl[l] = [] topRatedMonthArticles: props.topMonthArticles
bl[l].push(s)
}) })
setByLayout(bl)
console.log('[home] some grouped by layout articles are ready') const articles = createMemo(() => getSortedArticles())
const { getRandomTopics, getSortedTopics, getTopTopics } = useTopicsStore({
randomTopics: props.randomTopics
})
const { getTopAuthors } = useAuthorsStore()
// FIXME
// createEffect(() => {
// if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) {
// console.debug('[home] ' + getRandomTopics().length.toString() + ' random topics loaded')
// console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded')
// console.log('[home] preparing published articles...')
// // get shouts lists by
// const bl: { [key: string]: Shout[] } = {}
// const bt: { [key: string]: Shout[] } = {}
// articles().forEach((s: Shout) => {
// // by topic
// s.topics?.forEach(({ slug }: any) => {
// if (!bt[slug || '']) bt[slug || ''] = []
// bt[slug as string].push(s)
// })
// // by layout
// const l = s.layout || 'article'
// if (!bl[l]) bl[l] = []
// bl[l].push(s)
// })
// setByLayout(bl)
// setByTopic(bt)
// console.log('[home] some grouped articles are ready')
// }
// }, [articles()])
// FIXME
// createEffect(() => {
// if (Object.keys(byLayout()).length > 0 && getSortedTopics()) {
// // random special layout pick
// const special = LAYOUTS.filter((la) => la !== 'article')
// const layout = special[Math.floor(Math.random() * special.length)]
// setSomeLayout(byLayout()[layout])
// setSelectedLayout(layout)
// console.log(`[home] <${layout}> layout picked`)
// }
// }, [byLayout()])
const loadMore = () => {
// FIXME
const page = (props.page || 1) + 1
loadPublishedArticles({ page })
} }
}, [articles()])
createEffect(() => {
if (getSortedTopics() && !selectedLayout()) {
// random special layout pick
const special = LAYOUTS.filter((la) => la !== 'article')
const layout = special[Math.floor(Math.random() * special.length)]
setSomeLayout(byLayout()[layout])
setSelectedLayout(layout)
console.log(`[home] <${layout}> layout picked`)
}
}, [byLayout()])
onMount(() => {
if (props.topOverallArticles) setTopRated(props.topOverallArticles)
console.info('[home] mounted')
})
const getRandomTopics = () => shuffle(getSortedTopics()).slice(0, 12)
return ( return (
<Suspense fallback={t('Loading')}> <Suspense fallback={t('Loading')}>
<Show when={Boolean(articles())}> <Show when={articles().length > 0}>
<NavTopics topics={getRandomTopics()} /> <NavTopics topics={getRandomTopics()} />
<Row5 articles={articles().slice(0, 5) as []} /> <Row5 articles={articles().slice(0, 5)} />
<Hero /> <Hero />
<Beside <Beside
beside={articles().slice(5, 6)[0] as Shout} beside={articles().slice(5, 6)[0]}
title={t('Top viewed')} title={t('Top viewed')}
values={topViewed()} values={getTopViewedArticles().slice(0, 5)}
wrapper={'top-article'} wrapper={'top-article'}
/> />
<Row3 articles={articles().slice(6, 9) as []} /> <Row3 articles={articles().slice(6, 9)} />
<Beside <Beside
beside={articles().slice(9, 10)[0] as Shout} beside={articles().slice(9, 10)[0]}
title={t('Top authors')} title={t('Top authors')}
values={topAuthors()} values={getTopAuthors().slice(0, 5)}
wrapper={'author'} wrapper={'author'}
/> />
<Slider title={t('Top month articles')} articles={topRatedMonth()} /> <Slider title={t('Top month articles')} articles={getTopRatedMonthArticles()} />
<Row2 articles={articles().slice(10, 12) as []} /> <Row2 articles={articles().slice(10, 12)} />
<RowShort articles={articles().slice(12, 16) as []} /> <RowShort articles={articles().slice(12, 16)} />
<Row1 article={articles().slice(16, 17)[0] as Shout} /> <Row1 article={articles().slice(16, 17)[0]} />
<Row3 articles={articles().slice(17, 20) as []} /> <Row3 articles={articles().slice(17, 20)} />
<Row3 articles={topCommented()} header={<h2>{t('Top commented')}</h2>} /> <Row3 articles={getTopCommentedArticles()} header={<h2>{t('Top commented')}</h2>} />
<Group <Group
articles={someLayout()} articles={someLayout()}
header={ header={
@ -116,24 +132,24 @@ export const HomePage = (props: HomeProps) => {
} }
/> />
<Slider title={t('Favorite')} articles={topRated()} /> <Slider title={t('Favorite')} articles={getTopRatedArticles()} />
<Beside <Beside
beside={articles().slice(20, 21)[0] as Shout} beside={articles().slice(20, 21)[0]}
title={t('Top topics')} title={t('Top topics')}
values={topTopics()} values={getTopTopics().slice(0, 5)}
wrapper={'topic'} wrapper={'topic'}
isTopicCompact={true} isTopicCompact={true}
/> />
<Row3 articles={articles().slice(21, 24) as []} /> <Row3 articles={articles().slice(21, 24)} />
<Banner /> <Banner />
<Row2 articles={articles().slice(24, 26) as []} /> <Row2 articles={articles().slice(24, 26)} />
<Row3 articles={articles().slice(26, 29) as []} /> <Row3 articles={articles().slice(26, 29)} />
<Row2 articles={articles().slice(29, 31) as []} /> <Row2 articles={articles().slice(29, 31)} />
<Row3 articles={articles().slice(31, 34) as []} /> <Row3 articles={articles().slice(31, 34)} />
<p class="load-more-container"> <p class="load-more-container">
<button class="button" onClick={loadMorePublished}> <button class="button" onClick={loadMore}>
{t('Load more')} {t('Load more')}
</button> </button>
</p> </p>

View File

@ -4,58 +4,38 @@ import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { params } from '../../stores/router' import { params } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles' import { useArticlesStore, loadSearchResults } from '../../stores/zine/articles'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
type Props = { type Props = {
query: string
results: Shout[] results: Shout[]
} }
export const SearchPage = (props: Props) => { export const SearchPage = (props: Props) => {
const args = useStore(params) const args = useStore(params)
const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results }) const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results })
const [getQuery, setQuery] = createSignal(props.query)
// FIXME server sort const handleQueryChange = (ev) => {
// const [q, setq] = createSignal(props?.q || '') setQuery(ev.target.value)
// const articles = createMemo(() => { }
// const sorted = sortBy(articles(), by() || byRelevance)
// return q().length > 3 const handleSubmit = (ev) => {
// ? sorted.filter( // TODO page
// (a) => // TODO sort
// a.title?.toLowerCase().includes(q().toLowerCase()) || loadSearchResults({ query: getQuery() })
// a.body?.toLowerCase().includes(q().toLowerCase()) }
// )
// : sorted
// })
//
// function handleQueryChange(ev) {
// const el = ev.target as HTMLInputElement
// const query = el.value
// setq(query)
// }
//
// function handleSubmit(ev) {
// ev.preventDefault()
// const el = ev.target as HTMLInputElement
// const query = el.value
// setq(query)
// setBy('')
// }
return ( return (
<div class="search-page wide-container"> <div class="search-page wide-container">
<form action="/search" class="search-form row"> <form action="/search" class="search-form row">
<div class="col-sm-9"> <div class="col-sm-9">
{/*FIXME*/} {/*FIXME t*/}
{/*<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />*/} <input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />
<input type="search" name="q" placeholder="Введите текст..." />
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
{/*FIXME*/} <button class="button" type="submit" onClick={handleSubmit}>
{/*<button class="button" type="submit" onClick={handleSubmit}>*/}
{/* {t('Search')}*/}
{/*</button>*/}
<button class="button" type="submit">
{t('Search')} {t('Search')}
</button> </button>
</div> </div>

View File

@ -1,4 +1,4 @@
import { For, Show, createMemo, createEffect, createSignal } from 'solid-js' import { For, Show, createMemo } from 'solid-js'
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import Row3 from '../Feed/Row3' import Row3 from '../Feed/Row3'
import Row2 from '../Feed/Row2' import Row2 from '../Feed/Row2'
@ -8,11 +8,10 @@ import '../../styles/Topic.scss'
import { FullTopic } from '../Topic/Full' import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { params } from '../../stores/router' import { params } from '../../stores/router'
import { useArticlesStore } from '../../stores/zine/articles'
import { useStore } from '@nanostores/solid'
import { unique } from '../../utils'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { byCreated, sortBy } from '../../utils/sortby' import { useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useStore } from '@nanostores/solid'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -21,22 +20,20 @@ interface TopicProps {
export const TopicPage = (props: TopicProps) => { export const TopicPage = (props: TopicProps) => {
const args = useStore(params) const args = useStore(params)
const { getArticlesByTopics } = useArticlesStore({ sortedArticles: props.topicArticles }) const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
const [topicAuthors, setTopicAuthors] = createSignal([])
const sortedArticles = createMemo(() => {
const aaa = getArticlesByTopics()[props.topic.slug] || []
aaa.forEach((a: Shout) => {
a.topics?.forEach((t: Topic) => {
if (props.topic.slug === t.slug) {
setTopicAuthors((aaa) => [...aaa, a])
}
})
})
return args()['by'] ? sortBy(aaa, args()['by']) : sortBy(aaa, byCreated)
})
const { getTopicEntities } = useTopicsStore({ topics: [props.topic] }) const { getTopicEntities } = useTopicsStore({ topics: [props.topic] })
const topic = createMemo(() => getTopicEntities()[props.topic.slug] || props.topic)
const { getAuthorsByTopic } = useAuthorsStore()
const topic = createMemo(() => getTopicEntities()[props.topic.slug])
/*
const slug = createMemo<string>(() => {
let slug = props?.slug
if (props?.slug.startsWith('@')) slug = slug.replace('@', '')
return slug
})
*/
const title = createMemo(() => { const title = createMemo(() => {
const m = args()['by'] const m = args()['by']
@ -88,9 +85,9 @@ export const TopicPage = (props: TopicProps) => {
<div class="row"> <div class="row">
<h3 class="col-12">{title()}</h3> <h3 class="col-12">{title()}</h3>
<For each={sortedArticles().slice(0, 6)}> <For each={sortedArticles().slice(0, 6)}>
{(a: Shout) => ( {(article) => (
<div class="col-md-6"> <div class="col-md-6">
<ArticleCard article={a} /> <ArticleCard article={article} />
</div> </div>
)} )}
</For> </For>
@ -102,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={unique(topicAuthors()) as any} values={getAuthorsByTopic()[topic().slug]}
beside={sortedArticles()[6]} beside={sortedArticles()[6]}
wrapper={'author'} wrapper={'author'}
/> />

View File

@ -1,7 +1,7 @@
import { gql } from '@urql/core' import { gql } from '@urql/core'
export default gql` export default gql`
query RefreshSessionMutation { mutation RefreshSessionMutation {
refreshSession { refreshSession {
error error
token token

View File

@ -1,19 +1,19 @@
--- ---
import { ArticlePage } from '../components/Views/ArticlePage' import { ArticlePage } from '../components/Views/ArticlePage'
import type { Shout } from '../graphql/types.gen'
import Zine from '../layouts/zine.astro' import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
const slug = Astro.params.slug as string const slug = Astro.params.slug as string
let article: Shout
if (slug.includes('/') || slug.includes('.map') || slug.includes('.ico')) { if (slug.includes('/') || slug.includes('.map')) {
Astro.redirect('/404') return Astro.redirect('/404')
} else {
article = await apiClient.getArticle({ slug })
if (!article) Astro.redirect('/404')
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
} }
const article = await apiClient.getArticle({ slug })
if (!article) {
return Astro.redirect('/404')
}
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
--- ---
<Zine> <Zine>

View File

@ -5,6 +5,8 @@ import { apiClient } from '../../../utils/apiClient'
const slug = Astro.params.slug.toString() const slug = Astro.params.slug.toString()
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug] }) const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug] })
// FIXME get author by slugs
const author = articles[0].authors.find((a) => a.slug === slug) const author = articles[0].authors.find((a) => a.slug === slug)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate') Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')

View File

@ -3,11 +3,11 @@ import { FeedPage } from '../../components/Views/Feed'
import Zine from '../../layouts/zine.astro' import Zine from '../../layouts/zine.astro'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
const recentArticles = await apiClient.getRecentAllArticles({}) const recentArticles = await apiClient.getRecentArticles({ page: 1 })
const shoutSlugs = recentArticles.map((s) => s.slug) const shoutSlugs = recentArticles.map((s) => s.slug)
const reactions = await apiClient.getReactionsForShouts({ shoutSlugs }) const reactions = await apiClient.getReactionsForShouts({ shoutSlugs })
--- ---
<Zine> <Zine>
<FeedPage recentArticles={recentArticles} reactions={reactions} client:load /> <FeedPage articles={recentArticles} reactions={reactions} client:load />
</Zine> </Zine>

View File

@ -4,7 +4,7 @@ import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
const randomTopics = await apiClient.getRandomTopics() const randomTopics = await apiClient.getRandomTopics()
const recentPublished = await apiClient.getRecentPublishedArticles({}) const recentPublished = await apiClient.getRecentPublishedArticles({ page: 1 })
const topMonth = await apiClient.getTopMonthArticles() const topMonth = await apiClient.getTopMonthArticles()
const topOverall = await apiClient.getTopArticles() const topOverall = await apiClient.getTopArticles()
@ -15,7 +15,7 @@ Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate'
<HomePage <HomePage
randomTopics={randomTopics} randomTopics={randomTopics}
recentPublishedArticles={recentPublished} recentPublishedArticles={recentPublished}
topMonthArticles={[]} topMonthArticles={topMonth}
topOverallArticles={topOverall} topOverallArticles={topOverall}
client:load client:load
/> />

View File

@ -5,9 +5,9 @@ import { apiClient } from '../utils/apiClient'
const params: URLSearchParams = Astro.url.searchParams const params: URLSearchParams = Astro.url.searchParams
const q = params.get('q') const q = params.get('q')
const results = await apiClient.getSearchResults({ query: q, page: 1, size: 50 }) const results = await apiClient.getSearchResults({ query: q })
--- ---
<Zine> <Zine>
<SearchPage results={results || []} client:load /> <SearchPage results={results} query={q} client:load />
</Zine> </Zine>

View File

@ -8,51 +8,51 @@ const log = getLogger('auth-store')
export const session = atom<AuthResult>() export const session = atom<AuthResult>()
export const signIn = action(session, 'signIn', async (store, params) => { export const signIn = async (params) => {
const s = await apiClient.signIn(params) const s = await apiClient.signIn(params)
store.set(s) session.set(s)
setToken(s.token) setToken(s.token)
log.debug('signed in') log.debug('signed in')
}) }
export const signUp = action(session, 'signUp', async (store, params) => { export const signUp = async (params) => {
const s = await apiClient.signUp(params) const s = await apiClient.signUp(params)
store.set(s) session.set(s)
setToken(s.token) setToken(s.token)
log.debug('signed up') log.debug('signed up')
}) }
export const signOut = action(session, 'signOut', (store) => { export const signOut = () => {
store.set(null) session.set(null)
resetToken() resetToken()
log.debug('signed out') log.debug('signed out')
}) }
export const emailChecks = atom<{ [email: string]: boolean }>({}) export const emailChecks = atom<{ [email: string]: boolean }>({})
export const signCheck = action(emailChecks, 'signCheck', async (store, params) => { export const signCheck = async (params) => {
store.set(await apiClient.signCheck(params)) emailChecks.set(await apiClient.signCheck(params))
}) }
export const resetCode = atom<string>() export const resetCode = atom<string>()
export const signReset = action(resetCode, 'signReset', async (_store, params) => { export const signReset = async (params) => {
await apiClient.signReset(params) // { email } await apiClient.signReset(params) // { email }
resetToken() resetToken()
}) }
export const signResend = action(resetCode, 'signResend', async (_store, params) => { export const signResend = async (params) => {
await apiClient.signResend(params) // { email } await apiClient.signResend(params) // { email }
}) }
export const signResetConfirm = action(session, 'signResetConfirm', async (store, params) => { export const signResetConfirm = async (params) => {
const auth = await apiClient.signResetConfirm(params) // { code } const auth = await apiClient.signResetConfirm(params) // { code }
setToken(auth.token) setToken(auth.token)
store.set(auth) session.set(auth)
}) }
export const renewSession = action(session, 'renewSession', async (store) => { export const renewSession = async () => {
const s = await apiClient.getSession() // token in header const s = await apiClient.getSession() // token in header
setToken(s.token) setToken(s.token)
store.set(s) session.set(s)
}) }

View File

@ -1,5 +1,5 @@
import { persistentAtom } from '@nanostores/persistent' import { persistentAtom } from '@nanostores/persistent'
import { action, atom } from 'nanostores' import { atom } from 'nanostores'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
export const locale = persistentAtom<string>('locale', 'ru') export const locale = persistentAtom<string>('locale', 'ru')

View File

@ -1,14 +1,20 @@
import { atom } from 'nanostores' import { atom, computed, ReadableAtom } from 'nanostores'
import type { Shout } from '../../graphql/types.gen' import type { Author, Shout, Topic } from '../../graphql/types.gen'
import type { WritableAtom } from 'nanostores' import type { WritableAtom } from 'nanostores'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { params } from '../router' import { addAuthorsByTopic } from './authors'
import { addTopicsByAuthor } from './topics'
import { byStat } from '../../utils/sortby'
let articleEntitiesStore: WritableAtom<Record<string, Shout>> let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }>
let sortedArticlesStore: WritableAtom<Shout[]> let sortedArticlesStore: WritableAtom<Shout[]>
let articlesByAuthorsStore: WritableAtom<Record<string, Shout[]>> let topRatedArticlesStore: WritableAtom<Shout[]>
let articlesByTopicsStore: WritableAtom<Record<string, Shout[]>> let topRatedMonthArticlesStore: WritableAtom<Shout[]>
let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }>
let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }>
let topViewedArticlesStore: ReadableAtom<Shout[]>
let topCommentedArticlesStore: ReadableAtom<Shout[]>
const initStore = (initial?: Record<string, Shout>) => { const initStore = (initial?: Record<string, Shout>) => {
if (articleEntitiesStore) { if (articleEntitiesStore) {
@ -16,11 +22,51 @@ const initStore = (initial?: Record<string, Shout>) => {
} }
articleEntitiesStore = atom<Record<string, Shout>>(initial) articleEntitiesStore = atom<Record<string, Shout>>(initial)
articlesByAuthorsStore = computed(articleEntitiesStore, (articleEntities) => {
return Object.values(articleEntities).reduce((acc, article) => {
article.authors.forEach((author) => {
if (!acc[author.slug]) {
acc[author.slug] = []
}
acc[author.slug].push(article)
})
return acc
}, {} as { [authorSlug: string]: Shout[] })
})
articlesByTopicsStore = computed(articleEntitiesStore, (articleEntities) => {
return Object.values(articleEntities).reduce((acc, article) => {
article.topics.forEach((topic) => {
if (!acc[topic.slug]) {
acc[topic.slug] = []
}
acc[topic.slug].push(article)
})
return acc
}, {} as { [authorSlug: string]: Shout[] })
})
topViewedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
const sortedArticles = Object.values(articleEntities)
sortedArticles.sort(byStat('viewed'))
return sortedArticles
})
topCommentedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
const sortedArticles = Object.values(articleEntities)
sortedArticles.sort(byStat('commented'))
return sortedArticles
})
} }
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const addArticles = (articles: Shout[]) => { const addArticles = (...args: Shout[][]) => {
const newArticleEntities = articles.reduce((acc, article) => { const allArticles = args.flatMap((articles) => articles || [])
const newArticleEntities = allArticles.reduce((acc, article) => {
acc[article.slug] = article acc[article.slug] = article
return acc return acc
}, {} as Record<string, Shout>) }, {} as Record<string, Shout>)
@ -34,75 +80,121 @@ const addArticles = (articles: Shout[]) => {
}) })
} }
const authorsByTopic = allArticles.reduce((acc, article) => {
const { authors, topics } = article
topics.forEach((topic) => {
if (!acc[topic.slug]) {
acc[topic.slug] = []
}
authors.forEach((author) => {
if (!acc[topic.slug].some((a) => a.slug === author.slug)) {
acc[topic.slug].push(author)
}
})
})
return acc
}, {} as { [topicSlug: string]: Author[] })
addAuthorsByTopic(authorsByTopic)
const topicsByAuthor = allArticles.reduce((acc, article) => {
const { authors, topics } = article
authors.forEach((author) => {
if (!acc[author.slug]) {
acc[author.slug] = []
}
topics.forEach((topic) => {
if (!acc[author.slug].some((t) => t.slug === topic.slug)) {
acc[author.slug].push(topic)
}
})
})
return acc
}, {} as { [authorSlug: string]: Topic[] })
addTopicsByAuthor(topicsByAuthor)
}
const addSortedArticles = (articles: Shout[]) => {
if (!sortedArticlesStore) { if (!sortedArticlesStore) {
sortedArticlesStore = atom<Shout[]>(articles) sortedArticlesStore = atom(articles)
} else { return
}
if (articles) {
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles]) sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
} }
const groupedByAuthors: Record<string, Shout[]> = {}
const groupedByTopics: Record<string, Shout[]> = {}
if (!articlesByTopicsStore || !articlesByAuthorsStore) {
articles.forEach((a) => {
a.authors.forEach((author) => {
if (!groupedByAuthors[author.slug]) groupedByAuthors[author.slug] = []
groupedByAuthors[author.slug].push(a)
})
a.topics.forEach((t) => {
if (!groupedByTopics[t.slug]) groupedByTopics[t.slug] = []
groupedByTopics[t.slug].push(a)
})
})
}
if (!articlesByTopicsStore) {
articlesByTopicsStore = atom<Record<string, Shout[]>>(groupedByTopics)
} else {
// TODO: deep update logix needed here
articlesByTopicsStore.set({ ...articlesByTopicsStore.get(), ...groupedByTopics })
}
if (!articlesByAuthorsStore) {
articlesByAuthorsStore = atom<Record<string, Shout[]>>(groupedByAuthors)
} else {
// TODO: deep update logix needed here too
articlesByAuthorsStore.set({ ...articlesByAuthorsStore.get(), ...groupedByAuthors })
}
} }
export const loadRecentAllArticles = async ({ page }: { page: number }): Promise<void> => { export const loadRecentArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getRecentAllArticles({ page, size: 50 }) const newArticles = await apiClient.getRecentArticles({ page })
addArticles(newArticles) addArticles(newArticles)
addSortedArticles(newArticles)
} }
export const loadRecentPublishedArticles = async ({ page }: { page: number }): Promise<void> => { export const loadPublishedArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getRecentPublishedArticles({ page, size: 50 }) const newArticles = await apiClient.getPublishedArticles({ page })
addArticles(newArticles) addArticles(newArticles)
addSortedArticles(newArticles)
}
export const loadSearchResults = async ({ query }: { query: string }): Promise<void> => {
const newArticles = await apiClient.getSearchResults({ query })
addArticles(newArticles)
addSortedArticles(newArticles)
} }
type InitialState = { type InitialState = {
sortedArticles?: Shout[] sortedArticles?: Shout[]
topRatedArticles?: Shout[]
topRatedMonthArticles?: Shout[]
} }
export const useArticlesStore = ({ sortedArticles }: InitialState) => { export const useArticlesStore = ({
addArticles(sortedArticles) sortedArticles,
topRatedArticles,
topRatedMonthArticles
}: InitialState = {}) => {
addArticles(sortedArticles, topRatedArticles, topRatedMonthArticles)
addSortedArticles(sortedArticles)
if (!topRatedArticlesStore) {
topRatedArticlesStore = atom(topRatedArticles)
} else {
topRatedArticlesStore.set(topRatedArticles)
}
if (!topRatedMonthArticlesStore) {
topRatedMonthArticlesStore = atom(topRatedMonthArticles)
} else {
topRatedMonthArticlesStore.set(topRatedMonthArticles)
}
const getArticleEntities = useStore(articleEntitiesStore) const getArticleEntities = useStore(articleEntitiesStore)
const getSortedArticles = useStore(sortedArticlesStore) const getSortedArticles = useStore(sortedArticlesStore)
const getArticlesByAuthors = useStore(articlesByAuthorsStore) const getTopRatedArticles = useStore(topRatedArticlesStore)
const getArticlesByTopics = useStore(articlesByTopicsStore) const getTopRatedMonthArticles = useStore(topRatedMonthArticlesStore)
const getArticlesByAuthor = useStore(articlesByAuthorsStore)
const getArticlesByTopic = useStore(articlesByTopicsStore)
// TODO: get from server
const getTopViewedArticles = useStore(topViewedArticlesStore)
// TODO: get from server
const getTopCommentedArticles = useStore(topCommentedArticlesStore)
return { getArticleEntities, getSortedArticles, getArticlesByTopics, getArticlesByAuthors } return {
} getArticleEntities,
getSortedArticles,
export const loadMoreAll = () => { getArticlesByTopic,
const searchParams = useStore(params) getArticlesByAuthor,
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1 getTopRatedArticles,
loadRecentAllArticles({ page: pn + 1 }) getTopViewedArticles,
} getTopCommentedArticles,
getTopRatedMonthArticles
export const loadMorePublished = () => { }
const searchParams = useStore(params)
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1
loadRecentPublishedArticles({ page: pn + 1 })
} }

View File

@ -3,23 +3,25 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
import { atom, computed } from 'nanostores' import { atom, computed } from 'nanostores'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { byCreated } from '../../utils/sortby' import { byCreated, byStat } from '../../utils/sortby'
export type AuthorsSortBy = 'created' | 'name' export type AuthorsSortBy = 'created' | 'name'
const sortByStore = atom<AuthorsSortBy>('created') const sortAllByStore = atom<AuthorsSortBy>('created')
let authorEntitiesStore: WritableAtom<Record<string, Author>> let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }>
let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }>
let sortedAuthorsStore: ReadableAtom<Author[]> let sortedAuthorsStore: ReadableAtom<Author[]>
let authorsByTopicStore: WritableAtom<Record<string, Author[]>> let topAuthorsStore: ReadableAtom<Author[]>
const initStore = (initial?: Record<string, Author>) => {
const initStore = (initial: { [authorSlug: string]: Author }) => {
if (authorEntitiesStore) { if (authorEntitiesStore) {
return return
} }
authorEntitiesStore = atom<Record<string, Author>>(initial) authorEntitiesStore = atom(initial)
sortedAuthorsStore = computed([authorEntitiesStore, sortByStore], (authorEntities, sortBy) => { sortedAuthorsStore = computed([authorEntitiesStore, sortAllByStore], (authorEntities, sortBy) => {
const authors = Object.values(authorEntities) const authors = Object.values(authorEntities)
switch (sortBy) { switch (sortBy) {
case 'created': { case 'created': {
@ -27,12 +29,21 @@ const initStore = (initial?: Record<string, Author>) => {
break break
} }
case 'name': { case 'name': {
// FIXME authors.sort((a, b) => a.name.localeCompare(b.name))
break break
} }
} }
return authors return authors
}) })
topAuthorsStore = computed(authorEntitiesStore, (authorEntities) => {
// TODO real top authors
return Object.values(authorEntities)
})
}
export const setSortAllBy = (sortBy: AuthorsSortBy) => {
sortAllByStore.set(sortBy)
} }
const addAuthors = (authors: Author[]) => { const addAuthors = (authors: Author[]) => {
@ -51,16 +62,47 @@ const addAuthors = (authors: Author[]) => {
} }
} }
export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[] }) => {
const allAuthors = Object.values(authorsByTopic).flat()
addAuthors(allAuthors)
if (!authorsByTopicStore) {
authorsByTopicStore = atom<{ [topicSlug: string]: Author[] }>(authorsByTopic)
} else {
const newState = Object.entries(authorsByTopic).reduce((acc, [topicSlug, authors]) => {
if (!acc[topicSlug]) {
acc[topicSlug] = []
}
authors.forEach((author) => {
if (!acc[topicSlug].some((a) => a.slug === author.slug)) {
acc[topicSlug].push(author)
}
})
return acc
}, authorsByTopicStore.get())
authorsByTopicStore.set(newState)
}
}
export const loadAllAuthors = async (): Promise<void> => { export const loadAllAuthors = async (): Promise<void> => {
const authors = await apiClient.getAllAuthors() const authors = await apiClient.getAllAuthors()
addAuthors(authors) addAuthors(authors)
} }
export const useAuthorsStore = (initial?: Author[]) => { type InitialState = {
if (initial) addAuthors(initial) authors?: Author[]
}
export const useAuthorsStore = ({ authors }: InitialState = {}) => {
addAuthors(authors || [])
const getAuthorEntities = useStore(authorEntitiesStore) const getAuthorEntities = useStore(authorEntitiesStore)
const getSortedAuthors = useStore(sortedAuthorsStore) const getSortedAuthors = useStore(sortedAuthorsStore)
const getAuthorsByTopic = useStore(authorsByTopicStore) const getAuthorsByTopic = useStore(authorsByTopicStore)
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic } const getTopAuthors = useStore(topAuthorsStore)
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic, getTopAuthors }
} }

View File

@ -3,6 +3,7 @@ import { apiClient } from '../../utils/apiClient'
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
await apiClient.follow({ what, slug }) await apiClient.follow({ what, slug })
// refresh session
// TODO: _store update code // TODO: _store update code
} }
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {

View File

@ -4,12 +4,19 @@ import { useStore } from '@nanostores/solid'
let currentArticleStore: WritableAtom<Shout | null> let currentArticleStore: WritableAtom<Shout | null>
// TODO add author, topic? type InitialState = {
export const useCurrentArticleStore = (initialState: Shout) => { currentArticle: Shout
}
export const useCurrentArticleStore = ({ currentArticle }: InitialState) => {
if (!currentArticleStore) { if (!currentArticleStore) {
currentArticleStore = atom(initialState) currentArticleStore = atom(currentArticle)
} }
// FIXME
// addTopicsByAuthor
// addAuthorsByTopic
const getCurrentArticle = useStore(currentArticleStore) const getCurrentArticle = useStore(currentArticleStore)
return { return {

View File

@ -1,11 +1,13 @@
import { action, atom, WritableAtom } from 'nanostores' import { atom, WritableAtom } from 'nanostores'
import type { Reaction } from '../../graphql/types.gen' import type { Reaction } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { reduceBy } from '../../utils/reduce' import { reduceBy } from '../../utils/reduce'
export let reactionsOrdered: WritableAtom<Reaction[]> // FIXME
export const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
let reactionsOrdered: WritableAtom<Reaction[]>
const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
export const useReactionsStore = (initial?: Reaction[]) => { export const useReactionsStore = (initial?: Reaction[]) => {
if (!reactionsOrdered) { if (!reactionsOrdered) {
@ -41,18 +43,14 @@ export const loadReactions = async ({
reactionsOrdered.set(reactions) reactionsOrdered.set(reactions)
} }
export const createReaction = async (reaction: Reaction) =>
// FIXME
reactionsOrdered.set(await apiClient.createReaction({ reaction }))
export const createReaction = (reaction) => export const updateReaction = async (reaction: Reaction) =>
action(reactionsOrdered, 'createReaction', async (store) => { // FIXME
store.set(await apiClient.createReaction({ reaction })) reactionsOrdered.set(await apiClient.updateReaction({ reaction }))
})
export const updateReaction = (reaction) => export const deleteReaction = async (reactionId: number) =>
action(reactionsOrdered, 'updateReaction', async (store) => { // FIXME
store.set(await apiClient.updateReaction({ reaction })) reactionsOrdered.set(await apiClient.destroyReaction({ id: reactionId }))
})
export const deleteReaction = (reaction_id) =>
action(reactionsOrdered, 'deleteReaction', async (store) => {
store.set(await apiClient.destroyReaction({ id: reaction_id }))
})

View File

@ -1,7 +1,7 @@
import { persistentAtom } from '@nanostores/persistent' import { persistentAtom } from '@nanostores/persistent'
import { action } from 'nanostores' import { useStore } from '@nanostores/solid'
export const seen = persistentAtom<{ [slug: string]: Date }>( const seen = persistentAtom<{ [slug: string]: Date }>(
'seen', 'seen',
{}, {},
{ {
@ -10,9 +10,9 @@ export const seen = persistentAtom<{ [slug: string]: Date }>(
} }
) )
export const addSeen = export const addSeen = (slug) => seen.set({ ...seen.get(), [slug]: Date.now() })
(slug) => action(
seen, export const useSeenStore = () => {
'addSeen', const getSeen = useStore(seen)
(s) => s.set({ ...s.get(), [slug]: Date.now() }) return { getSeen }
) }

View File

@ -1,49 +0,0 @@
// derived solid stores for top lists
import { createSignal } from 'solid-js'
import type { Author, Shout, Topic } from '../../graphql/types.gen'
import { sortBy } from '../../utils/sortby'
import { useTopicsStore } from './topics'
const [topTopics, setTopTopics] = createSignal([] as Topic[])
const [topAuthors, setTopAuthors] = createSignal([] as Author[])
const [topViewed, setTopViewed] = createSignal([] as Shout[])
const [topCommented, setTopCommented] = createSignal([] as Shout[])
const [topReacted, setTopReacted] = createSignal([] as Shout[])
const [topRated, setTopRated] = createSignal([] as Shout[])
const [topRatedMonth, setTopRatedMonth] = createSignal([] as Shout[])
const nowtime = new Date()
export const setTops = async (aaa: Shout[]) => {
const ta = new Set([] as Author[])
const tt = {}
aaa.forEach((s: Shout) => {
s.topics?.forEach((tpc: Topic) => {
if (tpc?.slug) tt[tpc.slug] = tpc
})
s.authors?.forEach((a: Author) => a && ta.add(a))
})
const { getSortedTopics } = useTopicsStore(tt)
// setTopTopics(getSortedTopics().slice(0, 5)) // fallback
setTopTopics(sortBy(getSortedTopics(), 'shouts').slice(0, 5)) // test with TopicStat fixed
setTopAuthors(sortBy([...ta], 'shouts').slice(0, 5)) // TODO: expecting Author's stat metrics
setTopViewed(sortBy([...aaa], 'viewed').slice(0, 5))
setTopCommented(sortBy([...aaa], 'commented').slice(0, 5))
setTopReacted(sortBy([...aaa], 'reacted').slice(0, 5))
setTopRated(sortBy([...aaa], 'rating').slice(0, 5))
setTopRatedMonth(
sortBy(
[...aaa].filter((a: Shout) => {
const d = new Date(a.createdAt)
return (
d.getFullYear() === nowtime.getFullYear() &&
(d.getMonth() === nowtime.getMonth() || d.getMonth() === nowtime.getMonth() - 1)
)
}),
'rating'
)
)
console.log('[zine] top lists updated')
}
export { topTopics, topViewed, topCommented, topReacted, topAuthors, topRated, topRatedMonth, setTopRated }

View File

@ -3,14 +3,18 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
import { atom, computed } from 'nanostores' import { atom, computed } from 'nanostores'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { byCreated } from '../../utils/sortby' import { byCreated, byStat } from '../../utils/sortby'
import type { AuthorsSortBy } from './authors'
export type TopicsSortBy = 'created' | 'name' export type TopicsSortBy = 'created' | 'name'
const sortByStore = atom<TopicsSortBy>('created') const sortAllByStore = atom<TopicsSortBy>('created')
let topicEntitiesStore: WritableAtom<Record<string, Topic>> let topicEntitiesStore: WritableAtom<{ [topicSlug: string]: Topic }>
let sortedTopicsStore: ReadableAtom<Topic[]> let sortedTopicsStore: ReadableAtom<Topic[]>
let topTopicsStore: ReadableAtom<Topic[]>
let randomTopicsStore: WritableAtom<Topic[]>
let topicsByAuthorStore: WritableAtom<{ [authorSlug: string]: Topic[] }>
const initStore = (initial?: Record<string, Topic>) => { const initStore = (initial?: Record<string, Topic>) => {
if (topicEntitiesStore) { if (topicEntitiesStore) {
@ -19,7 +23,7 @@ const initStore = (initial?: Record<string, Topic>) => {
topicEntitiesStore = atom<Record<string, Topic>>(initial) topicEntitiesStore = atom<Record<string, Topic>>(initial)
sortedTopicsStore = computed([topicEntitiesStore, sortByStore], (topicEntities, sortBy) => { sortedTopicsStore = computed([topicEntitiesStore, sortAllByStore], (topicEntities, sortBy) => {
const topics = Object.values(topicEntities) const topics = Object.values(topicEntities)
switch (sortBy) { switch (sortBy) {
case 'created': { case 'created': {
@ -35,10 +39,24 @@ const initStore = (initial?: Record<string, Topic>) => {
} }
return topics return topics
}) })
topTopicsStore = computed(topicEntitiesStore, (topicEntities) => {
const topics = Object.values(topicEntities)
// DISCUSS
// topics.sort(byStat('shouts'))
topics.sort(byStat('rating'))
return topics
})
} }
const addTopics = (topics: Topic[] = []) => { export const setSortAllBy = (sortBy: TopicsSortBy) => {
const newTopicEntities = topics.reduce((acc, topic) => { sortAllByStore.set(sortBy)
}
const addTopics = (...args: Topic[][]) => {
const allTopics = args.flatMap((topics) => topics || [])
const newTopicEntities = allTopics.reduce((acc, topic) => {
acc[topic.slug] = topic acc[topic.slug] = topic
return acc return acc
}, {} as Record<string, Topic>) }, {} as Record<string, Topic>)
@ -53,6 +71,31 @@ const addTopics = (topics: Topic[] = []) => {
} }
} }
export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic[] }) => {
const allTopics = Object.values(topicsByAuthors).flat()
addTopics(allTopics)
if (!topicsByAuthorStore) {
topicsByAuthorStore = atom<{ [authorSlug: string]: Topic[] }>(topicsByAuthors)
} else {
const newState = Object.entries(topicsByAuthors).reduce((acc, [authorSlug, topics]) => {
if (!acc[authorSlug]) {
acc[authorSlug] = []
}
topics.forEach((topic) => {
if (!acc[authorSlug].some((t) => t.slug === topic.slug)) {
acc[authorSlug].push(topic)
}
})
return acc
}, topicsByAuthorStore.get())
topicsByAuthorStore.set(newState)
}
}
export const loadAllTopics = async (): Promise<void> => { export const loadAllTopics = async (): Promise<void> => {
const topics = await apiClient.getAllTopics() const topics = await apiClient.getAllTopics()
addTopics(topics) addTopics(topics)
@ -63,11 +106,18 @@ type InitialState = {
randomTopics?: Topic[] randomTopics?: Topic[]
} }
export const useTopicsStore = ({ topics }: InitialState) => { export const useTopicsStore = ({ topics, randomTopics }: InitialState = {}) => {
addTopics(topics) addTopics(topics, randomTopics)
if (!randomTopicsStore) {
randomTopicsStore = atom(randomTopics)
}
const getTopicEntities = useStore(topicEntitiesStore) const getTopicEntities = useStore(topicEntitiesStore)
const getSortedTopics = useStore(sortedTopicsStore) const getSortedTopics = useStore(sortedTopicsStore)
const getRandomTopics = useStore(randomTopicsStore)
const getTopicsByAuthor = useStore(topicsByAuthorStore)
const getTopTopics = useStore(topTopicsStore)
return { getTopicEntities, getSortedTopics } return { getTopicEntities, getSortedTopics, getRandomTopics, getTopicsByAuthor, getTopTopics }
} }

View File

@ -11,6 +11,7 @@ import reactionsForShouts from '../graphql/query/reactions-for-shouts'
import mySession from '../graphql/mutation/my-session' import mySession from '../graphql/mutation/my-session'
import { privateGraphQLClient } from '../graphql/privateGraphQLClient' import { privateGraphQLClient } from '../graphql/privateGraphQLClient'
import authLogout from '../graphql/mutation/auth-logout' import authLogout from '../graphql/mutation/auth-logout'
import authLogin from '../graphql/query/auth-login'
import authRegister from '../graphql/mutation/auth-register' import authRegister from '../graphql/mutation/auth-register'
import followMutation from '../graphql/mutation/follow' import followMutation from '../graphql/mutation/follow'
import unfollowMutation from '../graphql/mutation/unfollow' import unfollowMutation from '../graphql/mutation/unfollow'
@ -24,16 +25,22 @@ import authorsAll from '../graphql/query/authors-all'
import reactionCreate from '../graphql/mutation/reaction-create' import reactionCreate from '../graphql/mutation/reaction-create'
import reactionDestroy from '../graphql/mutation/reaction-destroy' import reactionDestroy from '../graphql/mutation/reaction-destroy'
import reactionUpdate from '../graphql/mutation/reaction-update' import reactionUpdate from '../graphql/mutation/reaction-update'
// import authorsBySlugs from '../graphql/query/authors-by-slugs'
import authLogin from '../graphql/query/auth-login'
import authCheck from '../graphql/query/auth-check' import authCheck from '../graphql/query/auth-check'
import authReset from '../graphql/mutation/auth-reset' 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'
const log = getLogger('api-client') const log = getLogger('api-client')
const FEED_PAGE_SIZE = 50
const REACTIONS_PAGE_SIZE = 100 // TODO: paging
const DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE = 999999
const DEFAULT_TOPIC_ARTICLES_PAGE_SIZE = 50
const DEFAULT_RECENT_ARTICLES_PAGE_SIZE = 50
const DEFAULT_REACTIONS_PAGE_SIZE = 50
const DEFAULT_SEARCH_RESULTS_PAGE_SIZE = 50
const DEFAULT_PUBLISHED_ARTICLES_PAGE_SIZE = 50
const DEFAULT_RANDOM_TOPICS_AMOUNT = 12
export const apiClient = { export const apiClient = {
getTopArticles: async () => { getTopArticles: async () => {
@ -44,15 +51,28 @@ export const apiClient = {
const response = await publicGraphQLClient.query(articlesTopMonth, { page: 1, size: 10 }).toPromise() const response = await publicGraphQLClient.query(articlesTopMonth, { page: 1, size: 10 }).toPromise()
return response.data.topMonth return response.data.topMonth
}, },
getRecentPublishedArticles: async ({
page = 1,
size = DEFAULT_RECENT_ARTICLES_PAGE_SIZE
}: {
page?: number
size?: number
}) => {
const response = await publicGraphQLClient.query(articlesRecentPublished, { page, size }).toPromise()
return response.data.recentPublished
},
getRandomTopics: async () => { getRandomTopics: async () => {
const response = await publicGraphQLClient.query(topicsRandomQuery, {}).toPromise() const response = await publicGraphQLClient
.query(topicsRandomQuery, { amount: DEFAULT_RANDOM_TOPICS_AMOUNT })
.toPromise()
return response.data.topicsRandom return response.data.topicsRandom
}, },
getSearchResults: async ({ getSearchResults: async ({
query, query,
page = 1, page = 1,
size = FEED_PAGE_SIZE size = DEFAULT_SEARCH_RESULTS_PAGE_SIZE
}: { }: {
query: string query: string
page?: number page?: number
@ -68,36 +88,26 @@ export const apiClient = {
return response.data.searchQuery return response.data.searchQuery
}, },
getRecentAllArticles: async ({ page, size }: { page?: number; size?: number }): Promise<Shout[]> => { getRecentArticles: async ({
const response = await publicGraphQLClient page = 1,
.query(articlesRecentAll, { size = DEFAULT_RECENT_ARTICLES_PAGE_SIZE
page: page || 1,
size: size || FEED_PAGE_SIZE
})
.toPromise()
return response.data.recentAll
},
getRecentPublishedArticles: async ({
page,
size
}: { }: {
page?: number page?: number
size?: number size?: number
}): Promise<Shout[]> => { }): Promise<Shout[]> => {
const response = await publicGraphQLClient const response = await publicGraphQLClient
.query(articlesRecentPublished, { .query(articlesRecentAll, {
page: page || 1, page,
size: size || FEED_PAGE_SIZE size
}) })
.toPromise() .toPromise()
return response.data.recentPublished return response.data.recentAll
}, },
getArticlesForTopics: async ({ getArticlesForTopics: async ({
topicSlugs, topicSlugs,
page = 1, page = 1,
size = FEED_PAGE_SIZE size = DEFAULT_TOPIC_ARTICLES_PAGE_SIZE
}: { }: {
topicSlugs: string[] topicSlugs: string[]
page?: number page?: number
@ -116,7 +126,7 @@ export const apiClient = {
getArticlesForAuthors: async ({ getArticlesForAuthors: async ({
authorSlugs, authorSlugs,
page = 1, page = 1,
size = FEED_PAGE_SIZE size = DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE
}: { }: {
authorSlugs: string[] authorSlugs: string[]
page?: number page?: number
@ -183,15 +193,24 @@ export const apiClient = {
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
return response.data.refreshSession return response.data.refreshSession
}, },
getPublishedArticles: async ({
page = 1,
size = DEFAULT_PUBLISHED_ARTICLES_PAGE_SIZE
}: {
page?: number
size?: number
}) => {
const response = await publicGraphQLClient.query(articlesRecentPublished, { page, size }).toPromise()
// feeds return response.data.recentPublished
},
getAllTopics: async () => { getAllTopics: async () => {
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise() const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
return response.data.topicsAll return response.data.topicsAll
}, },
getAllAuthors: async () => { getAllAuthors: async () => {
const response = await publicGraphQLClient.query(authorsAll, { page: 1, size: 9999 }).toPromise() const response = await publicGraphQLClient.query(authorsAll, { page: 1, size: 999999 }).toPromise()
return response.data.authorsAll return response.data.authorsAll
}, },
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => { getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
@ -205,7 +224,7 @@ export const apiClient = {
getReactionsForShouts: async ({ getReactionsForShouts: async ({
shoutSlugs, shoutSlugs,
page = 1, page = 1,
size = REACTIONS_PAGE_SIZE size = DEFAULT_REACTIONS_PAGE_SIZE
}: { }: {
shoutSlugs: string[] shoutSlugs: string[]
page?: number page?: number
@ -240,7 +259,10 @@ export const apiClient = {
return response.data.reactionsByShout return response.data.reactionsByShout
}, },
getAuthorsBySlugs: async ({ slugs }) => {
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise()
return response.data.getUsersBySlugs
},
createReaction: async ({ reaction }) => { createReaction: async ({ reaction }) => {
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise() const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise()
log.debug('[api] create reaction mutation called') log.debug('[api] create reaction mutation called')

View File

@ -1,3 +1,5 @@
import type { Stat } from '../graphql/types.gen'
export const byFirstChar = (a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || '') export const byFirstChar = (a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || '')
export const byCreated = (a: any, b: any) => { export const byCreated = (a: any, b: any) => {
@ -22,7 +24,8 @@ export const byLength = (a: any[], b: any[]) => {
return 0 return 0
} }
export const byStat = (metric: string) => { // FIXME keyof TopicStat
export const byStat = (metric: keyof Stat) => {
return (a, b) => { return (a, b) => {
const x = (a?.stat && a.stat[metric]) || 0 const x = (a?.stat && a.stat[metric]) || 0
const y = (b?.stat && b.stat[metric]) || 0 const y = (b?.stat && b.stat[metric]) || 0