store layer
This commit is contained in:
parent
4d1e7f7831
commit
c3c1c9fee3
|
@ -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
2
.stylelintignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.vercel/
|
||||||
|
dist/
|
|
@ -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')}*/}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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,20 @@ 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)
|
// TODO: setSubscribers(topic().stat?.followers as number + 1)
|
||||||
// // TODO: setSubscribers(topic().stat?.followers as number + 1)
|
} else {
|
||||||
// } else {
|
unfollow({ what: FollowingEntity.Topic, slug: topic().slug })
|
||||||
// const result = await apiClient.q(unfollow, { what: 'topic', slug: topic().slug })
|
// TODO: setSubscribers(topic().stat?.followers as number - 1)
|
||||||
// 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 +101,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">*/}
|
+ {t('Follow')}
|
||||||
{/* + {t('Follow')}*/}
|
</button>
|
||||||
{/* </button>*/}
|
}
|
||||||
{/* }*/}
|
>
|
||||||
{/*>*/}
|
<button onClick={() => subscribe(false)} class="button--light">
|
||||||
{/* <button onClick={() => subscribe(false)} class="button--light">*/}
|
- {t('Unfollow')}
|
||||||
{/* - {t('Unfollow')}*/}
|
</button>
|
||||||
{/* </button>*/}
|
</Show>
|
||||||
{/*</Show>*/}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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, 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,10 +10,8 @@ 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'
|
|
||||||
|
|
||||||
type AuthorProps = {
|
type AuthorProps = {
|
||||||
authorArticles: Shout[]
|
authorArticles: Shout[]
|
||||||
|
@ -20,20 +19,23 @@ type AuthorProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorPage = (props: AuthorProps) => {
|
export const AuthorPage = (props: AuthorProps) => {
|
||||||
const args = useStore(params)
|
const { getSortedArticles: articles, getArticlesByAuthor } = 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 +88,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}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -1,68 +1,63 @@
|
||||||
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
|
const page = (props.page || 1) + 1
|
||||||
// TODO: list of articles where you are co-author
|
loadRecentArticles({ page })
|
||||||
// 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 []
|
|
||||||
})
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="container feed">
|
<div class="container feed">
|
||||||
|
@ -103,8 +98,8 @@ export const FeedPage = (props: FeedProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="beside-column">
|
<ul class="beside-column">
|
||||||
<For each={topAuthors()}>
|
<For each={getTopAuthors()}>
|
||||||
{(author: Author) => (
|
{(author) => (
|
||||||
<li>
|
<li>
|
||||||
<AuthorCard author={author} compact={true} hasLink={true} />
|
<AuthorCard author={author} compact={true} hasLink={true} />
|
||||||
</li>
|
</li>
|
||||||
|
@ -125,22 +120,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()}>
|
||||||
{(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>
|
||||||
|
|
|
@ -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,97 @@ 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 {
|
||||||
|
getSortedArticles,
|
||||||
|
getTopRatedArticles,
|
||||||
|
getTopRatedMonthArticles,
|
||||||
|
getTopViewedArticles,
|
||||||
|
getTopCommentedArticles
|
||||||
|
} = useArticlesStore({
|
||||||
|
sortedArticles: props.recentPublishedArticles,
|
||||||
|
topRatedArticles: props.topOverallArticles,
|
||||||
|
topRatedMonthArticles: props.topMonthArticles
|
||||||
})
|
})
|
||||||
const { getSortedTopics } = useTopicsStore({ topics: sortBy(props.randomTopics, 'shouts') })
|
|
||||||
|
|
||||||
createEffect(() => {
|
const articles = createMemo(() => getSortedArticles())
|
||||||
if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) {
|
const { getRandomTopics, getSortedTopics, getTopTopics } = useTopicsStore({
|
||||||
console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded')
|
randomTopics: props.randomTopics
|
||||||
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 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 (
|
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 +131,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>
|
||||||
|
|
|
@ -4,57 +4,40 @@ 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*/}
|
<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />
|
||||||
{/*<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />*/}
|
|
||||||
<input type="search" name="q" 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')}
|
||||||
{/* {t('Search')}*/}
|
</button>
|
||||||
{/*</button>*/}
|
|
||||||
<button class="button" type="submit">
|
<button class="button" type="submit">
|
||||||
{t('Search')}
|
{t('Search')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -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'}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query RefreshSessionMutation {
|
query RefreshSessionQuery {
|
||||||
refreshSession {
|
refreshSession {
|
||||||
error
|
error
|
||||||
token
|
token
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
})
|
}
|
||||||
|
|
|
@ -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[]) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sortedArticlesStore) {
|
const authorsByTopic = allArticles.reduce((acc, article) => {
|
||||||
sortedArticlesStore = atom<Shout[]>(articles)
|
const { authors, topics } = article
|
||||||
} else {
|
|
||||||
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedByAuthors: Record<string, Shout[]> = {}
|
topics.forEach((topic) => {
|
||||||
const groupedByTopics: Record<string, Shout[]> = {}
|
if (!acc[topic.slug]) {
|
||||||
if (!articlesByTopicsStore || !articlesByAuthorsStore) {
|
acc[topic.slug] = []
|
||||||
articles.forEach((a) => {
|
}
|
||||||
a.authors.forEach((author) => {
|
|
||||||
if (!groupedByAuthors[author.slug]) groupedByAuthors[author.slug] = []
|
authors.forEach((author) => {
|
||||||
groupedByAuthors[author.slug].push(a)
|
if (!acc[topic.slug].some((a) => a.slug === author.slug)) {
|
||||||
})
|
acc[topic.slug].push(author)
|
||||||
a.topics.forEach((t) => {
|
}
|
||||||
if (!groupedByTopics[t.slug]) groupedByTopics[t.slug] = []
|
|
||||||
groupedByTopics[t.slug].push(a)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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) {
|
if (articles) {
|
||||||
articlesByTopicsStore = atom<Record<string, Shout[]>>(groupedByTopics)
|
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
|
||||||
} 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 })
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,23 @@ 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 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 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, sortByStore], (authorEntities, sortBy) => {
|
||||||
const authors = Object.values(authorEntities)
|
const authors = Object.values(authorEntities)
|
||||||
|
@ -27,12 +29,17 @@ 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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addAuthors = (authors: Author[]) => {
|
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> => {
|
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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 }))
|
|
||||||
})
|
|
||||||
|
|
|
@ -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 }
|
||||||
)
|
}
|
||||||
|
|
|
@ -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 }
|
|
|
@ -3,14 +3,17 @@ 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'
|
||||||
|
|
||||||
export type TopicsSortBy = 'created' | 'name'
|
export type TopicsSortBy = 'created' | 'name'
|
||||||
|
|
||||||
const sortByStore = atom<TopicsSortBy>('created')
|
const sortByStore = 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) {
|
||||||
|
@ -35,10 +38,20 @@ 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[] = []) => {
|
const addTopics = (...args: Topic[][]) => {
|
||||||
const newTopicEntities = topics.reduce((acc, 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 +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> => {
|
export const loadAllTopics = async (): Promise<void> => {
|
||||||
const topics = await apiClient.getAllTopics()
|
const topics = await apiClient.getAllTopics()
|
||||||
addTopics(topics)
|
addTopics(topics)
|
||||||
|
@ -63,11 +101,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 }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,21 @@ 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
|
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 = {
|
export const apiClient = {
|
||||||
getTopArticles: async () => {
|
getTopArticles: async () => {
|
||||||
|
@ -44,15 +50,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 +87,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 +125,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 +192,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 +223,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 +258,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')
|
||||||
|
|
|
@ -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,7 @@ export const byLength = (a: any[], b: any[]) => {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
export const byStat = (metric: string) => {
|
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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user