store layer

This commit is contained in:
Igor Lobanov 2022-09-13 11:59:04 +02:00
parent 4d1e7f7831
commit c3c1c9fee3
31 changed files with 709 additions and 553 deletions

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ const Link = (
<button
class={clsx('sidebar-link', props.className)}
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
// eslint-disable-next-line solid/reactivity
onClick={props.onClick}
disabled={props.disabled}
title={props.title}
@ -34,9 +33,52 @@ const Link = (
</button>
)
// FIXME
// eslint-disable-next-line sonarjs/cognitive-complexity
export default () => {
type FileLinkProps = {
file: File
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 [lastAction, setLastAction] = createSignal<string | undefined>()
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[] }) => (
// <span>
// <For each={props.keys}>{(k: string) => <i>{k}</i>}</For>
@ -229,7 +231,7 @@ export default () => {
<Show when={store.files?.length > 0}>
<h4>Files:</h4>
<p>
<For each={store.files}>{(file) => <FileLink file={file} />}</For>
<For each={store.files}>{(file) => <FileLink file={file} onOpenFile={onOpenFile} />}</For>
</p>
</Show>

View File

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

View File

@ -6,7 +6,7 @@ import { Form } from 'solid-js-form'
import { t } from '../../utils/intl'
import { hideModal, useModalStore } from '../../stores/ui'
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 { useValidator } from '../../utils/validators'

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +1,63 @@
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 Icon from '../Nav/Icon'
import { byCreated, sortBy } from '../../utils/sortby'
import { TopicCard } from '../Topic/Card'
import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card'
import { t } from '../../utils/intl'
import { useStore } from '@nanostores/solid'
import { FeedSidebar } from '../Feed/Sidebar'
import { session } from '../../stores/auth'
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 { useAuthorsStore } from '../../stores/zine/authors'
import { FeedSidebar } from '../Feed/Sidebar'
import { useTopicsStore } from '../../stores/zine/topics'
import { unique } from '../../utils'
import { AuthorCard } from '../Author/Card'
interface FeedProps {
recentArticles: Shout[]
articles: Shout[]
reactions: Reaction[]
page?: number
size?: number
}
const AUTHORSHIP_REACTIONS = [
ReactionKind.Accept,
ReactionKind.Reject,
ReactionKind.Propose,
ReactionKind.Ask
]
// const AUTHORSHIP_REACTIONS = [
// ReactionKind.Accept,
// ReactionKind.Reject,
// ReactionKind.Propose,
// ReactionKind.Ask
// ]
export const FeedPage = (props: FeedProps) => {
// state
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.recentArticles })
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles })
const reactions = useReactionsStore(props.reactions)
const {
// getAuthorEntities: authorsBySlug,
getSortedAuthors: authors
} = useAuthorsStore() // test if it catches preloaded authors
const { getTopAuthors, getSortedAuthors: authors } = useAuthorsStore()
const { getTopTopics } = useTopicsStore()
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 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[]>(() => {
// 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 loadMore = () => {
const page = (props.page || 1) + 1
loadRecentArticles({ page })
}
return (
<>
<div class="container feed">
@ -103,8 +98,8 @@ export const FeedPage = (props: FeedProps) => {
</div>
<ul class="beside-column">
<For each={topAuthors()}>
{(author: Author) => (
<For each={getTopAuthors()}>
{(author) => (
<li>
<AuthorCard author={author} compact={true} hasLink={true} />
</li>
@ -125,22 +120,23 @@ export const FeedPage = (props: FeedProps) => {
<aside class="col-md-3">
<section class="feed-comments">
<h4>{t('Comments')}</h4>
<For each={topReactions() as Reaction[]}>
{(c: Reaction) => <CommentCard comment={c} compact={true} />}
<For each={topReactions()}>
{(comment) => <CommentCard comment={comment} compact={true} />}
</For>
</section>
<Show when={getSortedTopics().length > 0}>
<Show when={getTopTopics().length > 0}>
<section class="feed-topics">
<h4>{t('Topics')}</h4>
<For each={getSortedTopics().slice(0, 5)}>
<For each={getTopTopics()}>
{(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />}
</For>
</section>
</Show>
</aside>
</div>
<p class="load-more-container">
<button class="button" onClick={loadMoreAll}>
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>

View File

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

View File

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

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 Row3 from '../Feed/Row3'
import Row2 from '../Feed/Row2'
@ -8,11 +8,10 @@ import '../../styles/Topic.scss'
import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl'
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 { byCreated, sortBy } from '../../utils/sortby'
import { useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useStore } from '@nanostores/solid'
interface TopicProps {
topic: Topic
@ -21,22 +20,20 @@ interface TopicProps {
export const TopicPage = (props: TopicProps) => {
const args = useStore(params)
const { getArticlesByTopics } = 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 { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
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 m = args()['by']
@ -88,9 +85,9 @@ export const TopicPage = (props: TopicProps) => {
<div class="row">
<h3 class="col-12">{title()}</h3>
<For each={sortedArticles().slice(0, 6)}>
{(a: Shout) => (
{(article) => (
<div class="col-md-6">
<ArticleCard article={a} />
<ArticleCard article={article} />
</div>
)}
</For>
@ -102,7 +99,7 @@ export const TopicPage = (props: TopicProps) => {
<Show when={sortedArticles().length > 5}>
<Beside
title={t('Topic is supported by')}
values={unique(topicAuthors()) as any}
values={getAuthorsByTopic()[topic().slug]}
beside={sortedArticles()[6]}
wrapper={'author'}
/>

View File

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

View File

@ -1,19 +1,19 @@
---
import { ArticlePage } from '../components/Views/ArticlePage'
import type { Shout } from '../graphql/types.gen'
import Zine from '../layouts/zine.astro'
import { apiClient } from '../utils/apiClient'
const slug = Astro.params.slug as string
let article: Shout
if (slug.includes('/') || slug.includes('.map') || slug.includes('.ico')) {
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')
if (slug.includes('/') || slug.includes('.map')) {
return Astro.redirect('/404')
}
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>

View File

@ -3,11 +3,11 @@ import { FeedPage } from '../../components/Views/Feed'
import Zine from '../../layouts/zine.astro'
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 reactions = await apiClient.getReactionsForShouts({ shoutSlugs })
---
<Zine>
<FeedPage recentArticles={recentArticles} reactions={reactions} client:load />
<FeedPage articles={recentArticles} reactions={reactions} client:load />
</Zine>

View File

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

View File

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

View File

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

View File

@ -1,14 +1,20 @@
import { atom } from 'nanostores'
import type { Shout } from '../../graphql/types.gen'
import { atom, computed, ReadableAtom } from 'nanostores'
import type { Author, Shout, Topic } from '../../graphql/types.gen'
import type { WritableAtom } from 'nanostores'
import { useStore } from '@nanostores/solid'
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 articlesByAuthorsStore: WritableAtom<Record<string, Shout[]>>
let articlesByTopicsStore: WritableAtom<Record<string, Shout[]>>
let topRatedArticlesStore: WritableAtom<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>) => {
if (articleEntitiesStore) {
@ -16,11 +22,51 @@ const initStore = (initial?: Record<string, Shout>) => {
}
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
const addArticles = (articles: Shout[]) => {
const newArticleEntities = articles.reduce((acc, article) => {
const addArticles = (...args: Shout[][]) => {
const allArticles = args.flatMap((articles) => articles || [])
const newArticleEntities = allArticles.reduce((acc, article) => {
acc[article.slug] = article
return acc
}, {} as Record<string, Shout>)
@ -34,75 +80,121 @@ const addArticles = (articles: Shout[]) => {
})
}
if (!sortedArticlesStore) {
sortedArticlesStore = atom<Shout[]>(articles)
} else {
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
}
const authorsByTopic = allArticles.reduce((acc, article) => {
const { authors, topics } = article
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)
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) {
sortedArticlesStore = atom(articles)
return
}
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 })
if (articles) {
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
}
}
export const loadRecentAllArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getRecentAllArticles({ page, size: 50 })
export const loadRecentArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getRecentArticles({ page })
addArticles(newArticles)
addSortedArticles(newArticles)
}
export const loadRecentPublishedArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getRecentPublishedArticles({ page, size: 50 })
export const loadPublishedArticles = async ({ page }: { page: number }): Promise<void> => {
const newArticles = await apiClient.getPublishedArticles({ page })
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 = {
sortedArticles?: Shout[]
topRatedArticles?: Shout[]
topRatedMonthArticles?: Shout[]
}
export const useArticlesStore = ({ sortedArticles }: InitialState) => {
addArticles(sortedArticles)
export const useArticlesStore = ({
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 getSortedArticles = useStore(sortedArticlesStore)
const getArticlesByAuthors = useStore(articlesByAuthorsStore)
const getArticlesByTopics = useStore(articlesByTopicsStore)
const getTopRatedArticles = useStore(topRatedArticlesStore)
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 }
}
export const loadMoreAll = () => {
const searchParams = useStore(params)
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1
loadRecentAllArticles({ page: pn + 1 })
}
export const loadMorePublished = () => {
const searchParams = useStore(params)
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1
loadRecentPublishedArticles({ page: pn + 1 })
return {
getArticleEntities,
getSortedArticles,
getArticlesByTopic,
getArticlesByAuthor,
getTopRatedArticles,
getTopViewedArticles,
getTopCommentedArticles,
getTopRatedMonthArticles
}
}

View File

@ -3,21 +3,23 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
import { atom, computed } from 'nanostores'
import type { Author } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid'
import { byCreated } from '../../utils/sortby'
import { byCreated, byStat } from '../../utils/sortby'
export type AuthorsSortBy = 'created' | 'name'
const sortByStore = 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 authorsByTopicStore: WritableAtom<Record<string, Author[]>>
const initStore = (initial?: Record<string, Author>) => {
let topAuthorsStore: ReadableAtom<Author[]>
const initStore = (initial: { [authorSlug: string]: Author }) => {
if (authorEntitiesStore) {
return
}
authorEntitiesStore = atom<Record<string, Author>>(initial)
authorEntitiesStore = atom(initial)
sortedAuthorsStore = computed([authorEntitiesStore, sortByStore], (authorEntities, sortBy) => {
const authors = Object.values(authorEntities)
@ -27,12 +29,17 @@ const initStore = (initial?: Record<string, Author>) => {
break
}
case 'name': {
// FIXME
authors.sort((a, b) => a.name.localeCompare(b.name))
break
}
}
return authors
})
topAuthorsStore = computed(authorEntitiesStore, (authorEntities) => {
// TODO real top authors
return Object.values(authorEntities)
})
}
const addAuthors = (authors: Author[]) => {
@ -51,16 +58,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> => {
const authors = await apiClient.getAllAuthors()
addAuthors(authors)
}
export const useAuthorsStore = (initial?: Author[]) => {
if (initial) addAuthors(initial)
type InitialState = {
authors?: Author[]
}
export const useAuthorsStore = ({ authors }: InitialState = {}) => {
addAuthors(authors || [])
const getAuthorEntities = useStore(authorEntitiesStore)
const getSortedAuthors = useStore(sortedAuthorsStore)
const getAuthorsByTopic = useStore(authorsByTopicStore)
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic }
const getTopAuthors = useStore(topAuthorsStore)
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic, getTopAuthors }
}

View File

@ -4,12 +4,19 @@ import { useStore } from '@nanostores/solid'
let currentArticleStore: WritableAtom<Shout | null>
// TODO add author, topic?
export const useCurrentArticleStore = (initialState: Shout) => {
type InitialState = {
currentArticle: Shout
}
export const useCurrentArticleStore = ({ currentArticle }: InitialState) => {
if (!currentArticleStore) {
currentArticleStore = atom(initialState)
currentArticleStore = atom(currentArticle)
}
// FIXME
// addTopicsByAuthor
// addAuthorsByTopic
const getCurrentArticle = useStore(currentArticleStore)
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 { useStore } from '@nanostores/solid'
import { apiClient } from '../../utils/apiClient'
import { reduceBy } from '../../utils/reduce'
export let reactionsOrdered: WritableAtom<Reaction[]>
export const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
// FIXME
let reactionsOrdered: WritableAtom<Reaction[]>
const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
export const useReactionsStore = (initial?: Reaction[]) => {
if (!reactionsOrdered) {
@ -41,18 +43,14 @@ export const loadReactions = async ({
reactionsOrdered.set(reactions)
}
export const createReaction = async (reaction: Reaction) =>
// FIXME
reactionsOrdered.set(await apiClient.createReaction({ reaction }))
export const createReaction = (reaction) =>
action(reactionsOrdered, 'createReaction', async (store) => {
store.set(await apiClient.createReaction({ reaction }))
})
export const updateReaction = async (reaction: Reaction) =>
// FIXME
reactionsOrdered.set(await apiClient.updateReaction({ reaction }))
export const updateReaction = (reaction) =>
action(reactionsOrdered, 'updateReaction', async (store) => {
store.set(await apiClient.updateReaction({ reaction }))
})
export const deleteReaction = (reaction_id) =>
action(reactionsOrdered, 'deleteReaction', async (store) => {
store.set(await apiClient.destroyReaction({ id: reaction_id }))
})
export const deleteReaction = async (reactionId: number) =>
// FIXME
reactionsOrdered.set(await apiClient.destroyReaction({ id: reactionId }))

View File

@ -1,7 +1,7 @@
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',
{},
{
@ -10,9 +10,9 @@ export const seen = persistentAtom<{ [slug: string]: Date }>(
}
)
export const addSeen =
(slug) => action(
seen,
'addSeen',
(s) => s.set({ ...s.get(), [slug]: Date.now() })
)
export const addSeen = (slug) => seen.set({ ...seen.get(), [slug]: Date.now() })
export const useSeenStore = () => {
const getSeen = useStore(seen)
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,17 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
import { atom, computed } from 'nanostores'
import type { Topic } from '../../graphql/types.gen'
import { useStore } from '@nanostores/solid'
import { byCreated } from '../../utils/sortby'
import { byCreated, byStat } from '../../utils/sortby'
export type TopicsSortBy = 'created' | 'name'
const sortByStore = atom<TopicsSortBy>('created')
let topicEntitiesStore: WritableAtom<Record<string, Topic>>
let topicEntitiesStore: WritableAtom<{ [topicSlug: string]: 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>) => {
if (topicEntitiesStore) {
@ -35,10 +38,20 @@ const initStore = (initial?: Record<string, Topic>) => {
}
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[] = []) => {
const newTopicEntities = topics.reduce((acc, topic) => {
const addTopics = (...args: Topic[][]) => {
const allTopics = args.flatMap((topics) => topics || [])
const newTopicEntities = allTopics.reduce((acc, topic) => {
acc[topic.slug] = topic
return acc
}, {} as Record<string, Topic>)
@ -53,6 +66,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> => {
const topics = await apiClient.getAllTopics()
addTopics(topics)
@ -63,11 +101,18 @@ type InitialState = {
randomTopics?: Topic[]
}
export const useTopicsStore = ({ topics }: InitialState) => {
addTopics(topics)
export const useTopicsStore = ({ topics, randomTopics }: InitialState = {}) => {
addTopics(topics, randomTopics)
if (!randomTopicsStore) {
randomTopicsStore = atom(randomTopics)
}
const getTopicEntities = useStore(topicEntitiesStore)
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 { privateGraphQLClient } from '../graphql/privateGraphQLClient'
import authLogout from '../graphql/mutation/auth-logout'
import authLogin from '../graphql/query/auth-login'
import authRegister from '../graphql/mutation/auth-register'
import followMutation from '../graphql/mutation/follow'
import unfollowMutation from '../graphql/mutation/unfollow'
@ -24,16 +25,21 @@ import authorsAll from '../graphql/query/authors-all'
import reactionCreate from '../graphql/mutation/reaction-create'
import reactionDestroy from '../graphql/mutation/reaction-destroy'
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 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'
const log = getLogger('api-client')
const FEED_PAGE_SIZE = 50
const REACTIONS_PAGE_SIZE = 100
const DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE = 50
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 = {
getTopArticles: async () => {
@ -44,15 +50,28 @@ export const apiClient = {
const response = await publicGraphQLClient.query(articlesTopMonth, { page: 1, size: 10 }).toPromise()
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 () => {
const response = await publicGraphQLClient.query(topicsRandomQuery, {}).toPromise()
const response = await publicGraphQLClient
.query(topicsRandomQuery, { amount: DEFAULT_RANDOM_TOPICS_AMOUNT })
.toPromise()
return response.data.topicsRandom
},
getSearchResults: async ({
query,
page = 1,
size = FEED_PAGE_SIZE
size = DEFAULT_SEARCH_RESULTS_PAGE_SIZE
}: {
query: string
page?: number
@ -68,36 +87,26 @@ export const apiClient = {
return response.data.searchQuery
},
getRecentAllArticles: async ({ page, size }: { page?: number; size?: number }): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(articlesRecentAll, {
page: page || 1,
size: size || FEED_PAGE_SIZE
})
.toPromise()
return response.data.recentAll
},
getRecentPublishedArticles: async ({
page,
size
getRecentArticles: async ({
page = 1,
size = DEFAULT_RECENT_ARTICLES_PAGE_SIZE
}: {
page?: number
size?: number
}): Promise<Shout[]> => {
const response = await publicGraphQLClient
.query(articlesRecentPublished, {
page: page || 1,
size: size || FEED_PAGE_SIZE
.query(articlesRecentAll, {
page,
size
})
.toPromise()
return response.data.recentPublished
return response.data.recentAll
},
getArticlesForTopics: async ({
topicSlugs,
page = 1,
size = FEED_PAGE_SIZE
size = DEFAULT_TOPIC_ARTICLES_PAGE_SIZE
}: {
topicSlugs: string[]
page?: number
@ -116,7 +125,7 @@ export const apiClient = {
getArticlesForAuthors: async ({
authorSlugs,
page = 1,
size = FEED_PAGE_SIZE
size = DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE
}: {
authorSlugs: string[]
page?: number
@ -183,15 +192,24 @@ export const apiClient = {
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
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 () => {
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
return response.data.topicsAll
},
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
},
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
@ -205,7 +223,7 @@ export const apiClient = {
getReactionsForShouts: async ({
shoutSlugs,
page = 1,
size = REACTIONS_PAGE_SIZE
size = DEFAULT_REACTIONS_PAGE_SIZE
}: {
shoutSlugs: string[]
page?: number
@ -240,7 +258,10 @@ export const apiClient = {
return response.data.reactionsByShout
},
getAuthorsBySlugs: async ({ slugs }) => {
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise()
return response.data.getUsersBySlugs
},
createReaction: async ({ reaction }) => {
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise()
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 byCreated = (a: any, b: any) => {
@ -22,7 +24,7 @@ export const byLength = (a: any[], b: any[]) => {
return 0
}
export const byStat = (metric: string) => {
export const byStat = (metric: keyof Stat) => {
return (a, b) => {
const x = (a?.stat && a.stat[metric]) || 0
const y = (b?.stat && b.stat[metric]) || 0