store layer
This commit is contained in:
parent
4d1e7f7831
commit
c3c1c9fee3
|
@ -3,3 +3,5 @@ public
|
|||
*.cjs
|
||||
src/graphql/*.gen.ts
|
||||
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 { renderMarkdown } from '@astrojs/markdown-remark'
|
||||
import { markdownOptions } from '../../../mdx.config'
|
||||
import { deleteReaction } from '../../stores/zine/reactions'
|
||||
|
||||
export default (props: {
|
||||
level?: number
|
||||
|
@ -20,16 +21,16 @@ export default (props: {
|
|||
const [body, setBody] = createSignal('')
|
||||
onMount(() => {
|
||||
const b: string = props.comment?.body
|
||||
if (b?.toString().startsWith('<')) setBody(b)
|
||||
else {
|
||||
if (b?.toString().startsWith('<')) {
|
||||
setBody(b)
|
||||
} else {
|
||||
renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
|
||||
}
|
||||
})
|
||||
const remove = () => {
|
||||
if (comment()?.id) {
|
||||
console.log('[comment] removing', comment().id)
|
||||
// FIXME
|
||||
// deleteReaction(comment().id)
|
||||
deleteReaction(comment().id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +80,7 @@ export default (props: {
|
|||
</button>
|
||||
|
||||
<Show when={props.canEdit}>
|
||||
{/*FIXME*/}
|
||||
{/*FIXME implement edit comment modal*/}
|
||||
{/*<button*/}
|
||||
{/* class="comment-control comment-control--edit"*/}
|
||||
{/* onClick={() => showModal('editComment')}*/}
|
||||
|
@ -93,7 +94,7 @@ export default (props: {
|
|||
</button>
|
||||
</Show>
|
||||
|
||||
{/*FIXME*/}
|
||||
{/*FIXME implement modals */}
|
||||
{/*<button*/}
|
||||
{/* class="comment-control comment-control--share"*/}
|
||||
{/* onClick={() => showModal('shareComment')}*/}
|
||||
|
|
|
@ -9,6 +9,9 @@ import { t } from '../../utils/intl'
|
|||
import { showModal } from '../../stores/ui'
|
||||
import { renderMarkdown } from '@astrojs/markdown-remark'
|
||||
import { markdownOptions } from '../../../mdx.config'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { session } from '../../stores/auth'
|
||||
|
||||
const MAX_COMMENT_LEVEL = 6
|
||||
|
||||
const getCommentLevel = (comment: Reaction, level = 0) => {
|
||||
|
@ -36,10 +39,14 @@ const formatDate = (date: Date) => {
|
|||
|
||||
export const FullArticle = (props: ArticleProps) => {
|
||||
const [body, setBody] = createSignal('')
|
||||
|
||||
const auth = useStore(session)
|
||||
|
||||
onMount(() => {
|
||||
const b: string = props.article?.body
|
||||
if (b?.toString().startsWith('<')) setBody(b)
|
||||
else {
|
||||
if (b?.toString().startsWith('<')) {
|
||||
setBody(b)
|
||||
} else {
|
||||
renderMarkdown(b, markdownOptions).then(({ code }) => setBody(code))
|
||||
}
|
||||
})
|
||||
|
@ -136,16 +143,15 @@ export const FullArticle = (props: ArticleProps) => {
|
|||
<div class="shout-stats__item shout-stats__item--date">{formattedDate}</div>
|
||||
</div>
|
||||
|
||||
{/*FIXME*/}
|
||||
{/*<div class="topics-list">*/}
|
||||
{/* <For each={props.article.topics}>*/}
|
||||
{/* {(topic) => (*/}
|
||||
{/* <div class="shout__topic">*/}
|
||||
{/* <a href={`/topic/${topic.slug}`}>{props.topicsBySlug[topic.slug].title}</a>*/}
|
||||
{/* </div>*/}
|
||||
{/* )}*/}
|
||||
{/* </For>*/}
|
||||
{/*</div>*/}
|
||||
<div class="topics-list">
|
||||
<For each={props.article.topics}>
|
||||
{(topic) => (
|
||||
<div class="shout__topic">
|
||||
<a href={`/topic/${topic.slug}`}>{topic.title}</a>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="shout__authors-list">
|
||||
<Show when={props.article?.authors?.length > 1}>
|
||||
|
@ -166,32 +172,28 @@ export const FullArticle = (props: ArticleProps) => {
|
|||
<ArticleComment
|
||||
comment={reaction}
|
||||
level={getCommentLevel(reaction)}
|
||||
// FIXME
|
||||
// canEdit={reaction.createdBy?.slug === session()?.user?.slug}
|
||||
canEdit={false}
|
||||
canEdit={reaction.createdBy?.slug === auth()?.user?.slug}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
|
||||
{/*FIXME*/}
|
||||
{/*<Show when={!session()?.user?.slug}>*/}
|
||||
{/* <div class="comment-warning" id="comments">*/}
|
||||
{/* {t('To leave a comment you please')}*/}
|
||||
{/* <a*/}
|
||||
{/* href={''}*/}
|
||||
{/* onClick={(evt) => {*/}
|
||||
{/* evt.preventDefault()*/}
|
||||
{/* showModal('auth')*/}
|
||||
{/* }}*/}
|
||||
{/* >*/}
|
||||
{/* <i>{t('sign up or sign in')}</i>*/}
|
||||
{/* </a>*/}
|
||||
{/* </div>*/}
|
||||
{/*</Show>*/}
|
||||
{/*<Show when={session()?.user?.slug}>*/}
|
||||
{/* <textarea class="write-comment" rows="1" placeholder={t('Write comment')} />*/}
|
||||
{/*</Show>*/}
|
||||
<Show when={!auth()?.user?.slug}>
|
||||
<div class="comment-warning" id="comments">
|
||||
{t('To leave a comment you please')}
|
||||
<a
|
||||
href={''}
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault()
|
||||
showModal('auth')
|
||||
}}
|
||||
>
|
||||
<i>{t('sign up or sign in')}</i>
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={auth()?.user?.slug}>
|
||||
<textarea class="write-comment" rows="1" placeholder={t('Write comment')} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -21,7 +21,6 @@ interface AuthorCardProps {
|
|||
}
|
||||
|
||||
export const AuthorCard = (props: AuthorCardProps) => {
|
||||
// const [zine, { follow, unfollow }] = useZine()
|
||||
const locale = useStore(locstore)
|
||||
const auth = useStore(session)
|
||||
const subscribed = createMemo(
|
||||
|
|
|
@ -24,7 +24,6 @@ const Link = (
|
|||
<button
|
||||
class={clsx('sidebar-link', props.className)}
|
||||
style={{ 'margin-bottom': props.withMargin ? '10px' : '' }}
|
||||
// eslint-disable-next-line solid/reactivity
|
||||
onClick={props.onClick}
|
||||
disabled={props.disabled}
|
||||
title={props.title}
|
||||
|
@ -34,9 +33,52 @@ const Link = (
|
|||
</button>
|
||||
)
|
||||
|
||||
// FIXME
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default () => {
|
||||
type FileLinkProps = {
|
||||
file: File
|
||||
onOpenFile: (file: File) => void
|
||||
}
|
||||
|
||||
const FileLink = (props: FileLinkProps) => {
|
||||
const length = 100
|
||||
let content = ''
|
||||
const getContent = (node: any) => {
|
||||
if (node.text) {
|
||||
content += node.text
|
||||
}
|
||||
|
||||
if (content.length > length) {
|
||||
content = `${content.slice(0, Math.max(0, length))}...`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
if (content.length >= length) {
|
||||
break
|
||||
}
|
||||
|
||||
content = getContent(child)
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const text = () =>
|
||||
props.file.path
|
||||
? props.file.path.slice(Math.max(0, props.file.path.length - length))
|
||||
: getContent(props.file.text?.doc)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line solid/no-react-specific-props
|
||||
<Link className="file" onClick={() => props.onOpenFile(props.file)} data-testid="open">
|
||||
{text()} {props.file.path && '📎'}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const Sidebar = () => {
|
||||
const [store, ctrl] = useState()
|
||||
const [lastAction, setLastAction] = createSignal<string | undefined>()
|
||||
const toggleTheme = () => {
|
||||
|
@ -102,46 +144,6 @@ export default () => {
|
|||
// })
|
||||
// }
|
||||
|
||||
const FileLink = (p: { file: File }) => {
|
||||
const length = 100
|
||||
let content = ''
|
||||
const getContent = (node: any) => {
|
||||
if (node.text) {
|
||||
content += node.text
|
||||
}
|
||||
|
||||
if (content.length > length) {
|
||||
content = `${content.slice(0, Math.max(0, length))}...`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
for (const child of node.content) {
|
||||
if (content.length >= length) {
|
||||
break
|
||||
}
|
||||
|
||||
content = getContent(child)
|
||||
}
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const text = () =>
|
||||
p.file.path
|
||||
? p.file.path.slice(Math.max(0, p.file.path.length - length))
|
||||
: getContent(p.file.text?.doc)
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line solid/no-react-specific-props
|
||||
<Link className="file" onClick={() => onOpenFile(p.file)} data-testid="open">
|
||||
{text()} {p.file.path && '📎'}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// const Keys = (props: { keys: string[] }) => (
|
||||
// <span>
|
||||
// <For each={props.keys}>{(k: string) => <i>{k}</i>}</For>
|
||||
|
@ -229,7 +231,7 @@ export default () => {
|
|||
<Show when={store.files?.length > 0}>
|
||||
<h4>Files:</h4>
|
||||
<p>
|
||||
<For each={store.files}>{(file) => <FileLink file={file} />}</For>
|
||||
<For each={store.files}>{(file) => <FileLink file={file} onOpenFile={onOpenFile} />}</For>
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
import { useStore } from '@nanostores/solid'
|
||||
import type { Author, Shout } from '../../graphql/types.gen'
|
||||
import { For } from 'solid-js'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { session } from '../../stores/auth'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { t } from '../../utils/intl'
|
||||
import Icon from '../Nav/Icon'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useSeenStore } from '../../stores/zine/seen'
|
||||
|
||||
type FeedSidebarProps = {
|
||||
authors: Author[]
|
||||
}
|
||||
|
||||
export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||
// const seen = useStore(seenstore)
|
||||
const { getSeen: seen } = useSeenStore()
|
||||
const auth = useStore(session)
|
||||
// const topics = useTopicsStore()
|
||||
const { getSortedAuthors: authors } = useAuthorsStore()
|
||||
// const articlesByTopics = useStore(abt)
|
||||
const { getSortedAuthors: authors } = useAuthorsStore({ authors: props.authors })
|
||||
const { getArticlesByTopic } = useArticlesStore()
|
||||
const { getTopicEntities } = useTopicsStore()
|
||||
|
||||
// const topicIsSeen = (topic: string) => {
|
||||
// let allSeen = false
|
||||
// articlesByTopics()[topic].forEach((s: Shout) => (allSeen = !seen()[s.slug]))
|
||||
// return allSeen
|
||||
// }
|
||||
//
|
||||
// const authorIsSeen = (slug: string) => {
|
||||
// return !!seen()[slug]
|
||||
// }
|
||||
const checkTopicIsSeen = (topicSlug: string) => {
|
||||
return getArticlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug]))
|
||||
}
|
||||
|
||||
const checkAuthorIsSeen = (authorSlug: string) => {
|
||||
return Boolean(seen()[authorSlug])
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -61,27 +63,27 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
|||
<strong>{t('My subscriptions')}</strong>
|
||||
</a>
|
||||
</li>
|
||||
{/*FIXME rework seen*/}
|
||||
{/*<For each={auth()?.info?.authors}>*/}
|
||||
{/* {(aslug) => (*/}
|
||||
{/* <li>*/}
|
||||
{/* <a href={`/author/${aslug}`} classList={{ unread: authorIsSeen(aslug) }}>*/}
|
||||
{/* <small>@{aslug}</small>*/}
|
||||
{/* {(authors()[aslug] as Author).name}*/}
|
||||
{/* </a>*/}
|
||||
{/* </li>*/}
|
||||
{/* )}*/}
|
||||
{/*</For>*/}
|
||||
|
||||
{/*<For each={auth()?.info?.topics as string[]}>*/}
|
||||
{/* {(topic: string) => (*/}
|
||||
{/* <li>*/}
|
||||
{/* <a href={`/author/${topic}`} classList={{ unread: topicIsSeen(topic) }}>*/}
|
||||
{/* {topics()[topic]?.title}*/}
|
||||
{/* </a>*/}
|
||||
{/* </li>*/}
|
||||
{/* )}*/}
|
||||
{/*</For>*/}
|
||||
<For each={auth()?.info?.authors}>
|
||||
{(authorSlug) => (
|
||||
<li>
|
||||
<a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}>
|
||||
<small>@{authorSlug}</small>
|
||||
{authors()[authorSlug].name}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<For each={auth()?.info?.topics}>
|
||||
{(topicSlug) => (
|
||||
<li>
|
||||
<a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}>
|
||||
{getTopicEntities()[topicSlug]?.title}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
|
||||
<p class="settings">
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Form } from 'solid-js-form'
|
|||
import { t } from '../../utils/intl'
|
||||
import { hideModal, useModalStore } from '../../stores/ui'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { session as sessionstore, signIn, renewSession } from '../../stores/auth'
|
||||
import { session as sessionstore, signIn } from '../../stores/auth'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import { useValidator } from '../../utils/validators'
|
||||
|
||||
|
|
|
@ -3,10 +3,12 @@ import { Show } from 'solid-js/web'
|
|||
import './Card.scss'
|
||||
import { createMemo } from 'solid-js'
|
||||
import type { Topic } from '../../graphql/types.gen'
|
||||
import { FollowingEntity } from '../../graphql/types.gen'
|
||||
import { t } from '../../utils/intl'
|
||||
import { locale as locstore } from '../../stores/ui'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { session } from '../../stores/auth'
|
||||
import { follow, unfollow } from '../../stores/zine/common'
|
||||
|
||||
interface TopicProps {
|
||||
topic: Topic
|
||||
|
@ -22,22 +24,20 @@ export const TopicCard = (props: TopicProps) => {
|
|||
|
||||
const topic = createMemo(() => props.topic)
|
||||
|
||||
// const subscribed = createMemo(() => {
|
||||
// return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false
|
||||
// })
|
||||
const subscribed = createMemo(() => {
|
||||
return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false
|
||||
})
|
||||
|
||||
// FIXME use store actions
|
||||
// const subscribe = async (really = true) => {
|
||||
// if (really) {
|
||||
// const result = await apiClient.q(follow, { what: 'topic', slug: topic().slug })
|
||||
// if (result.error) console.error(result.error)
|
||||
// // TODO: setSubscribers(topic().stat?.followers as number + 1)
|
||||
// } else {
|
||||
// const result = await apiClient.q(unfollow, { what: 'topic', slug: topic().slug })
|
||||
// if (result.error) console.error(result.error)
|
||||
// // TODO: setSubscribers(topic().stat?.followers as number - 1)
|
||||
// }
|
||||
// }
|
||||
const subscribe = async (really = true) => {
|
||||
if (really) {
|
||||
follow({ what: FollowingEntity.Topic, slug: topic().slug })
|
||||
// TODO: setSubscribers(topic().stat?.followers as number + 1)
|
||||
} else {
|
||||
unfollow({ what: FollowingEntity.Topic, slug: topic().slug })
|
||||
// TODO: setSubscribers(topic().stat?.followers as number - 1)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}>
|
||||
<div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}>
|
||||
|
@ -101,19 +101,18 @@ export const TopicCard = (props: TopicProps) => {
|
|||
</Show>
|
||||
</div>
|
||||
<div classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }}>
|
||||
{/*FIXME*/}
|
||||
{/*<Show*/}
|
||||
{/* when={subscribed()}*/}
|
||||
{/* fallback={*/}
|
||||
{/* <button onClick={() => subscribe(true)} class="button--light">*/}
|
||||
{/* + {t('Follow')}*/}
|
||||
{/* </button>*/}
|
||||
{/* }*/}
|
||||
{/*>*/}
|
||||
{/* <button onClick={() => subscribe(false)} class="button--light">*/}
|
||||
{/* - {t('Unfollow')}*/}
|
||||
{/* </button>*/}
|
||||
{/*</Show>*/}
|
||||
<Show
|
||||
when={subscribed()}
|
||||
fallback={
|
||||
<button onClick={() => subscribe(true)} class="button--light">
|
||||
+ {t('Follow')}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button onClick={() => subscribe(false)} class="button--light">
|
||||
- {t('Unfollow')}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@ interface ArticlePageProps {
|
|||
const ARTICLE_COMMENTS_PAGE_SIZE = 50
|
||||
|
||||
export const ArticlePage = (props: ArticlePageProps) => {
|
||||
const { getCurrentArticle } = useCurrentArticleStore(props.article)
|
||||
const { getCurrentArticle } = useCurrentArticleStore({ currentArticle: props.article })
|
||||
const [getCommentsPage] = createSignal(1)
|
||||
const [getIsCommentsLoading, setIsCommentsLoading] = createSignal(false)
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Show, createMemo } from 'solid-js'
|
||||
import type { Author, Shout, Topic } from '../../graphql/types.gen'
|
||||
import type { Author, Shout } from '../../graphql/types.gen'
|
||||
import Row2 from '../Feed/Row2'
|
||||
import Row3 from '../Feed/Row3'
|
||||
import Beside from '../Feed/Beside'
|
||||
import AuthorFull from '../Author/Full'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
|
@ -9,10 +10,8 @@ import { params } from '../../stores/router'
|
|||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
|
||||
import '../../styles/Topic.scss'
|
||||
import Beside from '../Feed/Beside'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { unique } from '../../utils'
|
||||
|
||||
type AuthorProps = {
|
||||
authorArticles: Shout[]
|
||||
|
@ -20,20 +19,23 @@ type AuthorProps = {
|
|||
}
|
||||
|
||||
export const AuthorPage = (props: AuthorProps) => {
|
||||
const args = useStore(params)
|
||||
const { getSortedArticles: articles, getArticlesByAuthors: articlesByAuthors } = useArticlesStore({
|
||||
const { getSortedArticles: articles, getArticlesByAuthor } = useArticlesStore({
|
||||
sortedArticles: props.authorArticles
|
||||
})
|
||||
const { getAuthorEntities: authors } = useAuthorsStore([props.author])
|
||||
const { getAuthorEntities: authors } = useAuthorsStore({ authors: [props.author] })
|
||||
const { getTopicsByAuthor } = useTopicsStore()
|
||||
|
||||
const author = createMemo(() => authors()[props.author.slug])
|
||||
const topics = createMemo(() => {
|
||||
const ttt = []
|
||||
articlesByAuthors()[author().slug].forEach((s: Shout) =>
|
||||
s.topics.forEach((tpc: Topic) => ttt.push(tpc))
|
||||
)
|
||||
return unique(ttt)
|
||||
const args = useStore(params)
|
||||
|
||||
//const slug = createMemo(() => author().slug)
|
||||
/*
|
||||
const slug = createMemo<string>(() => {
|
||||
let slug = props?.slug
|
||||
if (props?.slug.startsWith('@')) slug = slug.replace('@', '')
|
||||
return slug
|
||||
})
|
||||
const { getSortedTopics } = useTopicsStore({ topics: topics() })
|
||||
*/
|
||||
|
||||
const title = createMemo(() => {
|
||||
const m = args()['by']
|
||||
|
@ -86,7 +88,7 @@ export const AuthorPage = (props: AuthorProps) => {
|
|||
<Show when={articles()?.length > 0}>
|
||||
<Beside
|
||||
title={t('Topics which supported by author')}
|
||||
values={getSortedTopics()?.slice(0, 5)}
|
||||
values={getTopicsByAuthor()[author().slug].slice(0, 5)}
|
||||
beside={articles()[0]}
|
||||
wrapper={'topic'}
|
||||
topicShortDescription={true}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { State, StateContext } from '../Editor/prosemirror/context'
|
|||
import { createCtrl } from '../Editor/store/ctrl'
|
||||
import { Layout } from '../Editor/Layout'
|
||||
import Editor from '../Editor'
|
||||
import Sidebar from '../Editor/Sidebar'
|
||||
import { Sidebar } from '../Editor/Sidebar'
|
||||
import ErrorView from '../Editor/Error'
|
||||
import { newState } from '../Editor/store'
|
||||
|
||||
|
|
|
@ -1,68 +1,63 @@
|
|||
import { createMemo, For, Show } from 'solid-js'
|
||||
import { Shout, Reaction, ReactionKind, Topic, Author } from '../../graphql/types.gen'
|
||||
import type { Shout, Reaction } from '../../graphql/types.gen'
|
||||
import '../../styles/Feed.scss'
|
||||
import Icon from '../Nav/Icon'
|
||||
import { byCreated, sortBy } from '../../utils/sortby'
|
||||
import { TopicCard } from '../Topic/Card'
|
||||
import { ArticleCard } from '../Feed/Card'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
import { t } from '../../utils/intl'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { FeedSidebar } from '../Feed/Sidebar'
|
||||
import { session } from '../../stores/auth'
|
||||
import CommentCard from '../Article/Comment'
|
||||
import { loadMoreAll, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { FeedSidebar } from '../Feed/Sidebar'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { unique } from '../../utils'
|
||||
import { AuthorCard } from '../Author/Card'
|
||||
|
||||
interface FeedProps {
|
||||
recentArticles: Shout[]
|
||||
articles: Shout[]
|
||||
reactions: Reaction[]
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
const AUTHORSHIP_REACTIONS = [
|
||||
ReactionKind.Accept,
|
||||
ReactionKind.Reject,
|
||||
ReactionKind.Propose,
|
||||
ReactionKind.Ask
|
||||
]
|
||||
// const AUTHORSHIP_REACTIONS = [
|
||||
// ReactionKind.Accept,
|
||||
// ReactionKind.Reject,
|
||||
// ReactionKind.Propose,
|
||||
// ReactionKind.Ask
|
||||
// ]
|
||||
|
||||
export const FeedPage = (props: FeedProps) => {
|
||||
// state
|
||||
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.recentArticles })
|
||||
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles })
|
||||
const reactions = useReactionsStore(props.reactions)
|
||||
const {
|
||||
// getAuthorEntities: authorsBySlug,
|
||||
getSortedAuthors: authors
|
||||
} = useAuthorsStore() // test if it catches preloaded authors
|
||||
const { getTopAuthors, getSortedAuthors: authors } = useAuthorsStore()
|
||||
const { getTopTopics } = useTopicsStore()
|
||||
|
||||
const auth = useStore(session)
|
||||
const topics = createMemo(() => {
|
||||
const ttt = []
|
||||
articles().forEach((s: Shout) => s.topics.forEach((tpc: Topic) => ttt.push(tpc)))
|
||||
return unique(ttt)
|
||||
})
|
||||
const { getSortedTopics } = useTopicsStore({ topics: topics() })
|
||||
// derived
|
||||
|
||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||
const topAuthors = createMemo(() => sortBy(authors(), 'shouts'))
|
||||
// note this became synthetic
|
||||
|
||||
// methods
|
||||
// const expectingFocus = createMemo<Shout[]>(() => {
|
||||
// // 1 co-author notifications needs
|
||||
// // TODO: list of articles where you are co-author
|
||||
// // TODO: preload proposals
|
||||
// // TODO: (maybe?) and changes history
|
||||
// console.debug(reactions().filter((r) => r.kind in AUTHORSHIP_REACTIONS))
|
||||
//
|
||||
// // 2 community self-regulating mechanics
|
||||
// // TODO: query all new posts to be rated for publishing
|
||||
// // TODO: query all reactions where user is in authors list
|
||||
// return []
|
||||
// })
|
||||
|
||||
const expectingFocus = createMemo<Shout[]>(() => {
|
||||
// 1 co-author notifications needs
|
||||
// TODO: list of articles where you are co-author
|
||||
// TODO: preload proposals
|
||||
// TODO: (maybe?) and changes history
|
||||
console.debug(reactions().filter((r) => r.kind in AUTHORSHIP_REACTIONS))
|
||||
|
||||
// 2 community self-regulating mechanics
|
||||
// TODO: query all new posts to be rated for publishing
|
||||
// TODO: query all reactions where user is in authors list
|
||||
return []
|
||||
})
|
||||
const loadMore = () => {
|
||||
const page = (props.page || 1) + 1
|
||||
loadRecentArticles({ page })
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div class="container feed">
|
||||
|
@ -103,8 +98,8 @@ export const FeedPage = (props: FeedProps) => {
|
|||
</div>
|
||||
|
||||
<ul class="beside-column">
|
||||
<For each={topAuthors()}>
|
||||
{(author: Author) => (
|
||||
<For each={getTopAuthors()}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} compact={true} hasLink={true} />
|
||||
</li>
|
||||
|
@ -125,22 +120,23 @@ export const FeedPage = (props: FeedProps) => {
|
|||
<aside class="col-md-3">
|
||||
<section class="feed-comments">
|
||||
<h4>{t('Comments')}</h4>
|
||||
<For each={topReactions() as Reaction[]}>
|
||||
{(c: Reaction) => <CommentCard comment={c} compact={true} />}
|
||||
<For each={topReactions()}>
|
||||
{(comment) => <CommentCard comment={comment} compact={true} />}
|
||||
</For>
|
||||
</section>
|
||||
<Show when={getSortedTopics().length > 0}>
|
||||
<Show when={getTopTopics().length > 0}>
|
||||
<section class="feed-topics">
|
||||
<h4>{t('Topics')}</h4>
|
||||
<For each={getSortedTopics().slice(0, 5)}>
|
||||
<For each={getTopTopics()}>
|
||||
{(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />}
|
||||
</For>
|
||||
</section>
|
||||
</Show>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMoreAll}>
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
|
|
|
@ -13,19 +13,9 @@ import Group from '../Feed/Group'
|
|||
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||
import Icon from '../Nav/Icon'
|
||||
import { t } from '../../utils/intl'
|
||||
import {
|
||||
setTopRated,
|
||||
topRated,
|
||||
topViewed,
|
||||
topAuthors,
|
||||
topTopics,
|
||||
topRatedMonth,
|
||||
topCommented
|
||||
} from '../../stores/zine/top'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { loadMorePublished, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { sortBy } from '../../utils/sortby'
|
||||
import { shuffle } from '../../utils'
|
||||
import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
|
||||
type HomeProps = {
|
||||
randomTopics: Topic[]
|
||||
|
@ -41,72 +31,97 @@ const LAYOUTS = ['article', 'prose', 'music', 'video', 'image']
|
|||
export const HomePage = (props: HomeProps) => {
|
||||
const [someLayout, setSomeLayout] = createSignal([] as Shout[])
|
||||
const [selectedLayout, setSelectedLayout] = createSignal('article')
|
||||
const [byLayout, setByLayout] = createSignal<{ [layout: string]: Shout[] }>({})
|
||||
const { getSortedArticles: articles, getArticlesByTopics: byTopic } = useArticlesStore({
|
||||
sortedArticles: props.recentPublishedArticles
|
||||
const [byLayout, setByLayout] = createSignal({} as { [layout: string]: Shout[] })
|
||||
const [byTopic, setByTopic] = createSignal({} as { [topic: string]: Shout[] })
|
||||
|
||||
const {
|
||||
getSortedArticles,
|
||||
getTopRatedArticles,
|
||||
getTopRatedMonthArticles,
|
||||
getTopViewedArticles,
|
||||
getTopCommentedArticles
|
||||
} = useArticlesStore({
|
||||
sortedArticles: props.recentPublishedArticles,
|
||||
topRatedArticles: props.topOverallArticles,
|
||||
topRatedMonthArticles: props.topMonthArticles
|
||||
})
|
||||
const { getSortedTopics } = useTopicsStore({ topics: sortBy(props.randomTopics, 'shouts') })
|
||||
|
||||
createEffect(() => {
|
||||
if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) {
|
||||
console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded')
|
||||
console.log('[home] preparing published articles...')
|
||||
// get shouts lists by
|
||||
const bl: { [key: string]: Shout[] } = {}
|
||||
articles().forEach((s: Shout) => {
|
||||
// by layout
|
||||
const l = s.layout || 'article'
|
||||
if (!bl[l]) bl[l] = []
|
||||
bl[l].push(s)
|
||||
})
|
||||
setByLayout(bl)
|
||||
console.log('[home] some grouped by layout articles are ready')
|
||||
}
|
||||
}, [articles()])
|
||||
|
||||
createEffect(() => {
|
||||
if (getSortedTopics() && !selectedLayout()) {
|
||||
// random special layout pick
|
||||
const special = LAYOUTS.filter((la) => la !== 'article')
|
||||
const layout = special[Math.floor(Math.random() * special.length)]
|
||||
setSomeLayout(byLayout()[layout])
|
||||
setSelectedLayout(layout)
|
||||
console.log(`[home] <${layout}> layout picked`)
|
||||
}
|
||||
}, [byLayout()])
|
||||
|
||||
onMount(() => {
|
||||
if (props.topOverallArticles) setTopRated(props.topOverallArticles)
|
||||
console.info('[home] mounted')
|
||||
const articles = createMemo(() => getSortedArticles())
|
||||
const { getRandomTopics, getSortedTopics, getTopTopics } = useTopicsStore({
|
||||
randomTopics: props.randomTopics
|
||||
})
|
||||
const getRandomTopics = () => shuffle(getSortedTopics()).slice(0, 12)
|
||||
|
||||
const { getTopAuthors } = useAuthorsStore()
|
||||
|
||||
// FIXME
|
||||
// createEffect(() => {
|
||||
// if (articles() && articles().length > 0 && Object.keys(byTopic()).length === 0) {
|
||||
// console.debug('[home] ' + getRandomTopics().length.toString() + ' random topics loaded')
|
||||
// console.debug('[home] ' + articles().length.toString() + ' overall shouts loaded')
|
||||
// console.log('[home] preparing published articles...')
|
||||
// // get shouts lists by
|
||||
// const bl: { [key: string]: Shout[] } = {}
|
||||
// const bt: { [key: string]: Shout[] } = {}
|
||||
// articles().forEach((s: Shout) => {
|
||||
// // by topic
|
||||
// s.topics?.forEach(({ slug }: any) => {
|
||||
// if (!bt[slug || '']) bt[slug || ''] = []
|
||||
// bt[slug as string].push(s)
|
||||
// })
|
||||
// // by layout
|
||||
// const l = s.layout || 'article'
|
||||
// if (!bl[l]) bl[l] = []
|
||||
// bl[l].push(s)
|
||||
// })
|
||||
// setByLayout(bl)
|
||||
// setByTopic(bt)
|
||||
// console.log('[home] some grouped articles are ready')
|
||||
// }
|
||||
// }, [articles()])
|
||||
|
||||
// FIXME
|
||||
// createEffect(() => {
|
||||
// if (Object.keys(byLayout()).length > 0 && getSortedTopics()) {
|
||||
// // random special layout pick
|
||||
// const special = LAYOUTS.filter((la) => la !== 'article')
|
||||
// const layout = special[Math.floor(Math.random() * special.length)]
|
||||
// setSomeLayout(byLayout()[layout])
|
||||
// setSelectedLayout(layout)
|
||||
// console.log(`[home] <${layout}> layout picked`)
|
||||
// }
|
||||
// }, [byLayout()])
|
||||
|
||||
const loadMore = () => {
|
||||
const page = (props.page || 1) + 1
|
||||
loadPublishedArticles({ page })
|
||||
}
|
||||
return (
|
||||
<Suspense fallback={t('Loading')}>
|
||||
<Show when={Boolean(articles())}>
|
||||
<Show when={articles().length > 0}>
|
||||
<NavTopics topics={getRandomTopics()} />
|
||||
<Row5 articles={articles().slice(0, 5) as []} />
|
||||
<Row5 articles={articles().slice(0, 5)} />
|
||||
<Hero />
|
||||
<Beside
|
||||
beside={articles().slice(5, 6)[0] as Shout}
|
||||
beside={articles().slice(5, 6)[0]}
|
||||
title={t('Top viewed')}
|
||||
values={topViewed()}
|
||||
values={getTopViewedArticles().slice(0, 5)}
|
||||
wrapper={'top-article'}
|
||||
/>
|
||||
<Row3 articles={articles().slice(6, 9) as []} />
|
||||
<Row3 articles={articles().slice(6, 9)} />
|
||||
<Beside
|
||||
beside={articles().slice(9, 10)[0] as Shout}
|
||||
beside={articles().slice(9, 10)[0]}
|
||||
title={t('Top authors')}
|
||||
values={topAuthors()}
|
||||
values={getTopAuthors().slice(0, 5)}
|
||||
wrapper={'author'}
|
||||
/>
|
||||
|
||||
<Slider title={t('Top month articles')} articles={topRatedMonth()} />
|
||||
<Slider title={t('Top month articles')} articles={getTopRatedMonthArticles()} />
|
||||
|
||||
<Row2 articles={articles().slice(10, 12) as []} />
|
||||
<RowShort articles={articles().slice(12, 16) as []} />
|
||||
<Row1 article={articles().slice(16, 17)[0] as Shout} />
|
||||
<Row3 articles={articles().slice(17, 20) as []} />
|
||||
<Row3 articles={topCommented()} header={<h2>{t('Top commented')}</h2>} />
|
||||
<Row2 articles={articles().slice(10, 12)} />
|
||||
<RowShort articles={articles().slice(12, 16)} />
|
||||
<Row1 article={articles().slice(16, 17)[0]} />
|
||||
<Row3 articles={articles().slice(17, 20)} />
|
||||
<Row3 articles={getTopCommentedArticles()} header={<h2>{t('Top commented')}</h2>} />
|
||||
<Group
|
||||
articles={someLayout()}
|
||||
header={
|
||||
|
@ -116,24 +131,24 @@ export const HomePage = (props: HomeProps) => {
|
|||
}
|
||||
/>
|
||||
|
||||
<Slider title={t('Favorite')} articles={topRated()} />
|
||||
<Slider title={t('Favorite')} articles={getTopRatedArticles()} />
|
||||
|
||||
<Beside
|
||||
beside={articles().slice(20, 21)[0] as Shout}
|
||||
beside={articles().slice(20, 21)[0]}
|
||||
title={t('Top topics')}
|
||||
values={topTopics()}
|
||||
values={getTopTopics().slice(0, 5)}
|
||||
wrapper={'topic'}
|
||||
isTopicCompact={true}
|
||||
/>
|
||||
<Row3 articles={articles().slice(21, 24) as []} />
|
||||
<Row3 articles={articles().slice(21, 24)} />
|
||||
<Banner />
|
||||
<Row2 articles={articles().slice(24, 26) as []} />
|
||||
<Row3 articles={articles().slice(26, 29) as []} />
|
||||
<Row2 articles={articles().slice(29, 31) as []} />
|
||||
<Row3 articles={articles().slice(31, 34) as []} />
|
||||
<Row2 articles={articles().slice(24, 26)} />
|
||||
<Row3 articles={articles().slice(26, 29)} />
|
||||
<Row2 articles={articles().slice(29, 31)} />
|
||||
<Row3 articles={articles().slice(31, 34)} />
|
||||
|
||||
<p class="load-more-container">
|
||||
<button class="button" onClick={loadMorePublished}>
|
||||
<button class="button" onClick={loadMore}>
|
||||
{t('Load more')}
|
||||
</button>
|
||||
</p>
|
||||
|
|
|
@ -4,57 +4,40 @@ import type { Shout } from '../../graphql/types.gen'
|
|||
import { ArticleCard } from '../Feed/Card'
|
||||
import { t } from '../../utils/intl'
|
||||
import { params } from '../../stores/router'
|
||||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useArticlesStore, loadSearchResults } from '../../stores/zine/articles'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
|
||||
type Props = {
|
||||
query: string
|
||||
results: Shout[]
|
||||
}
|
||||
|
||||
export const SearchPage = (props: Props) => {
|
||||
const args = useStore(params)
|
||||
const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
||||
const [getQuery, setQuery] = createSignal(props.query)
|
||||
|
||||
// FIXME server sort
|
||||
// const [q, setq] = createSignal(props?.q || '')
|
||||
// const articles = createMemo(() => {
|
||||
// const sorted = sortBy(articles(), by() || byRelevance)
|
||||
// return q().length > 3
|
||||
// ? sorted.filter(
|
||||
// (a) =>
|
||||
// a.title?.toLowerCase().includes(q().toLowerCase()) ||
|
||||
// a.body?.toLowerCase().includes(q().toLowerCase())
|
||||
// )
|
||||
// : sorted
|
||||
// })
|
||||
//
|
||||
// function handleQueryChange(ev) {
|
||||
// const el = ev.target as HTMLInputElement
|
||||
// const query = el.value
|
||||
// setq(query)
|
||||
// }
|
||||
//
|
||||
// function handleSubmit(ev) {
|
||||
// ev.preventDefault()
|
||||
// const el = ev.target as HTMLInputElement
|
||||
// const query = el.value
|
||||
// setq(query)
|
||||
// setBy('')
|
||||
// }
|
||||
const handleQueryChange = (ev) => {
|
||||
setQuery(ev.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = (ev) => {
|
||||
// TODO page
|
||||
// TODO sort
|
||||
loadSearchResults({ query: getQuery() })
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="search-page wide-container">
|
||||
<form action="/search" class="search-form row">
|
||||
<div class="col-sm-9">
|
||||
{/*FIXME*/}
|
||||
{/*<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />*/}
|
||||
<input type="search" name="q" onChange={handleQueryChange} placeholder="Введите текст..." />
|
||||
<input type="search" name="q" placeholder="Введите текст..." />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{/*FIXME*/}
|
||||
{/*<button class="button" type="submit" onClick={handleSubmit}>*/}
|
||||
{/* {t('Search')}*/}
|
||||
{/*</button>*/}
|
||||
<button class="button" type="submit" onClick={handleSubmit}>
|
||||
{t('Search')}
|
||||
</button>
|
||||
<button class="button" type="submit">
|
||||
{t('Search')}
|
||||
</button>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { For, Show, createMemo, createEffect, createSignal } from 'solid-js'
|
||||
import { For, Show, createMemo } from 'solid-js'
|
||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||
import Row3 from '../Feed/Row3'
|
||||
import Row2 from '../Feed/Row2'
|
||||
|
@ -8,11 +8,10 @@ import '../../styles/Topic.scss'
|
|||
import { FullTopic } from '../Topic/Full'
|
||||
import { t } from '../../utils/intl'
|
||||
import { params } from '../../stores/router'
|
||||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { unique } from '../../utils'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { byCreated, sortBy } from '../../utils/sortby'
|
||||
import { useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
|
||||
interface TopicProps {
|
||||
topic: Topic
|
||||
|
@ -21,22 +20,20 @@ interface TopicProps {
|
|||
|
||||
export const TopicPage = (props: TopicProps) => {
|
||||
const args = useStore(params)
|
||||
const { getArticlesByTopics } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||
|
||||
const [topicAuthors, setTopicAuthors] = createSignal([])
|
||||
const sortedArticles = createMemo(() => {
|
||||
const aaa = getArticlesByTopics()[props.topic.slug] || []
|
||||
aaa.forEach((a: Shout) => {
|
||||
a.topics?.forEach((t: Topic) => {
|
||||
if (props.topic.slug === t.slug) {
|
||||
setTopicAuthors((aaa) => [...aaa, a])
|
||||
}
|
||||
})
|
||||
})
|
||||
return args()['by'] ? sortBy(aaa, args()['by']) : sortBy(aaa, byCreated)
|
||||
})
|
||||
const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||
const { getTopicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||
const topic = createMemo(() => getTopicEntities()[props.topic.slug] || props.topic)
|
||||
|
||||
const { getAuthorsByTopic } = useAuthorsStore()
|
||||
|
||||
const topic = createMemo(() => getTopicEntities()[props.topic.slug])
|
||||
|
||||
/*
|
||||
const slug = createMemo<string>(() => {
|
||||
let slug = props?.slug
|
||||
if (props?.slug.startsWith('@')) slug = slug.replace('@', '')
|
||||
return slug
|
||||
})
|
||||
*/
|
||||
|
||||
const title = createMemo(() => {
|
||||
const m = args()['by']
|
||||
|
@ -88,9 +85,9 @@ export const TopicPage = (props: TopicProps) => {
|
|||
<div class="row">
|
||||
<h3 class="col-12">{title()}</h3>
|
||||
<For each={sortedArticles().slice(0, 6)}>
|
||||
{(a: Shout) => (
|
||||
{(article) => (
|
||||
<div class="col-md-6">
|
||||
<ArticleCard article={a} />
|
||||
<ArticleCard article={article} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
@ -102,7 +99,7 @@ export const TopicPage = (props: TopicProps) => {
|
|||
<Show when={sortedArticles().length > 5}>
|
||||
<Beside
|
||||
title={t('Topic is supported by')}
|
||||
values={unique(topicAuthors()) as any}
|
||||
values={getAuthorsByTopic()[topic().slug]}
|
||||
beside={sortedArticles()[6]}
|
||||
wrapper={'author'}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { gql } from '@urql/core'
|
||||
|
||||
export default gql`
|
||||
query RefreshSessionMutation {
|
||||
query RefreshSessionQuery {
|
||||
refreshSession {
|
||||
error
|
||||
token
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
---
|
||||
import { ArticlePage } from '../components/Views/ArticlePage'
|
||||
import type { Shout } from '../graphql/types.gen'
|
||||
import Zine from '../layouts/zine.astro'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
|
||||
const slug = Astro.params.slug as string
|
||||
let article: Shout
|
||||
|
||||
if (slug.includes('/') || slug.includes('.map') || slug.includes('.ico')) {
|
||||
Astro.redirect('/404')
|
||||
} else {
|
||||
article = await apiClient.getArticle({ slug })
|
||||
if (!article) Astro.redirect('/404')
|
||||
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
||||
if (slug.includes('/') || slug.includes('.map')) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
const article = await apiClient.getArticle({ slug })
|
||||
if (!article) {
|
||||
return Astro.redirect('/404')
|
||||
}
|
||||
|
||||
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
|
||||
---
|
||||
|
||||
<Zine>
|
||||
|
|
|
@ -3,11 +3,11 @@ import { FeedPage } from '../../components/Views/Feed'
|
|||
import Zine from '../../layouts/zine.astro'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
|
||||
const recentArticles = await apiClient.getRecentAllArticles({})
|
||||
const recentArticles = await apiClient.getRecentArticles({ page: 1 })
|
||||
const shoutSlugs = recentArticles.map((s) => s.slug)
|
||||
const reactions = await apiClient.getReactionsForShouts({ shoutSlugs })
|
||||
---
|
||||
|
||||
<Zine>
|
||||
<FeedPage recentArticles={recentArticles} reactions={reactions} client:load />
|
||||
<FeedPage articles={recentArticles} reactions={reactions} client:load />
|
||||
</Zine>
|
||||
|
|
|
@ -4,7 +4,7 @@ import Zine from '../layouts/zine.astro'
|
|||
import { apiClient } from '../utils/apiClient'
|
||||
|
||||
const randomTopics = await apiClient.getRandomTopics()
|
||||
const recentPublished = await apiClient.getRecentPublishedArticles({})
|
||||
const recentPublished = await apiClient.getRecentPublishedArticles({ page: 1 })
|
||||
const topMonth = await apiClient.getTopMonthArticles()
|
||||
const topOverall = await apiClient.getTopArticles()
|
||||
|
||||
|
@ -15,7 +15,7 @@ Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate'
|
|||
<HomePage
|
||||
randomTopics={randomTopics}
|
||||
recentPublishedArticles={recentPublished}
|
||||
topMonthArticles={[]}
|
||||
topMonthArticles={topMonth}
|
||||
topOverallArticles={topOverall}
|
||||
client:load
|
||||
/>
|
||||
|
|
|
@ -5,9 +5,9 @@ import { apiClient } from '../utils/apiClient'
|
|||
|
||||
const params: URLSearchParams = Astro.url.searchParams
|
||||
const q = params.get('q')
|
||||
const results = await apiClient.getSearchResults({ query: q, page: 1, size: 50 })
|
||||
const results = await apiClient.getSearchResults({ query: q })
|
||||
---
|
||||
|
||||
<Zine>
|
||||
<SearchPage results={results || []} client:load />
|
||||
<SearchPage results={results} query={q} client:load />
|
||||
</Zine>
|
||||
|
|
|
@ -8,51 +8,51 @@ const log = getLogger('auth-store')
|
|||
|
||||
export const session = atom<AuthResult>()
|
||||
|
||||
export const signIn = action(session, 'signIn', async (store, params) => {
|
||||
export const signIn = async (params) => {
|
||||
const s = await apiClient.signIn(params)
|
||||
store.set(s)
|
||||
session.set(s)
|
||||
setToken(s.token)
|
||||
log.debug('signed in')
|
||||
})
|
||||
}
|
||||
|
||||
export const signUp = action(session, 'signUp', async (store, params) => {
|
||||
export const signUp = async (params) => {
|
||||
const s = await apiClient.signUp(params)
|
||||
store.set(s)
|
||||
session.set(s)
|
||||
setToken(s.token)
|
||||
log.debug('signed up')
|
||||
})
|
||||
}
|
||||
|
||||
export const signOut = action(session, 'signOut', (store) => {
|
||||
store.set(null)
|
||||
export const signOut = () => {
|
||||
session.set(null)
|
||||
resetToken()
|
||||
log.debug('signed out')
|
||||
})
|
||||
}
|
||||
|
||||
export const emailChecks = atom<{ [email: string]: boolean }>({})
|
||||
|
||||
export const signCheck = action(emailChecks, 'signCheck', async (store, params) => {
|
||||
store.set(await apiClient.signCheck(params))
|
||||
})
|
||||
export const signCheck = async (params) => {
|
||||
emailChecks.set(await apiClient.signCheck(params))
|
||||
}
|
||||
|
||||
export const resetCode = atom<string>()
|
||||
|
||||
export const signReset = action(resetCode, 'signReset', async (_store, params) => {
|
||||
export const signReset = async (params) => {
|
||||
await apiClient.signReset(params) // { email }
|
||||
resetToken()
|
||||
})
|
||||
}
|
||||
|
||||
export const signResend = action(resetCode, 'signResend', async (_store, params) => {
|
||||
export const signResend = async (params) => {
|
||||
await apiClient.signResend(params) // { email }
|
||||
})
|
||||
}
|
||||
|
||||
export const signResetConfirm = action(session, 'signResetConfirm', async (store, params) => {
|
||||
export const signResetConfirm = async (params) => {
|
||||
const auth = await apiClient.signResetConfirm(params) // { code }
|
||||
setToken(auth.token)
|
||||
store.set(auth)
|
||||
})
|
||||
session.set(auth)
|
||||
}
|
||||
|
||||
export const renewSession = action(session, 'renewSession', async (store) => {
|
||||
export const renewSession = async () => {
|
||||
const s = await apiClient.getSession() // token in header
|
||||
setToken(s.token)
|
||||
store.set(s)
|
||||
})
|
||||
session.set(s)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import { atom } from 'nanostores'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { atom, computed, ReadableAtom } from 'nanostores'
|
||||
import type { Author, Shout, Topic } from '../../graphql/types.gen'
|
||||
import type { WritableAtom } from 'nanostores'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import { params } from '../router'
|
||||
import { addAuthorsByTopic } from './authors'
|
||||
import { addTopicsByAuthor } from './topics'
|
||||
import { byStat } from '../../utils/sortby'
|
||||
|
||||
let articleEntitiesStore: WritableAtom<Record<string, Shout>>
|
||||
let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }>
|
||||
let sortedArticlesStore: WritableAtom<Shout[]>
|
||||
let articlesByAuthorsStore: WritableAtom<Record<string, Shout[]>>
|
||||
let articlesByTopicsStore: WritableAtom<Record<string, Shout[]>>
|
||||
let topRatedArticlesStore: WritableAtom<Shout[]>
|
||||
let topRatedMonthArticlesStore: WritableAtom<Shout[]>
|
||||
let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }>
|
||||
let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }>
|
||||
let topViewedArticlesStore: ReadableAtom<Shout[]>
|
||||
let topCommentedArticlesStore: ReadableAtom<Shout[]>
|
||||
|
||||
const initStore = (initial?: Record<string, Shout>) => {
|
||||
if (articleEntitiesStore) {
|
||||
|
@ -16,11 +22,51 @@ const initStore = (initial?: Record<string, Shout>) => {
|
|||
}
|
||||
|
||||
articleEntitiesStore = atom<Record<string, Shout>>(initial)
|
||||
|
||||
articlesByAuthorsStore = computed(articleEntitiesStore, (articleEntities) => {
|
||||
return Object.values(articleEntities).reduce((acc, article) => {
|
||||
article.authors.forEach((author) => {
|
||||
if (!acc[author.slug]) {
|
||||
acc[author.slug] = []
|
||||
}
|
||||
acc[author.slug].push(article)
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as { [authorSlug: string]: Shout[] })
|
||||
})
|
||||
|
||||
articlesByTopicsStore = computed(articleEntitiesStore, (articleEntities) => {
|
||||
return Object.values(articleEntities).reduce((acc, article) => {
|
||||
article.topics.forEach((topic) => {
|
||||
if (!acc[topic.slug]) {
|
||||
acc[topic.slug] = []
|
||||
}
|
||||
acc[topic.slug].push(article)
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as { [authorSlug: string]: Shout[] })
|
||||
})
|
||||
|
||||
topViewedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
|
||||
const sortedArticles = Object.values(articleEntities)
|
||||
sortedArticles.sort(byStat('viewed'))
|
||||
return sortedArticles
|
||||
})
|
||||
|
||||
topCommentedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
|
||||
const sortedArticles = Object.values(articleEntities)
|
||||
sortedArticles.sort(byStat('commented'))
|
||||
return sortedArticles
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const addArticles = (articles: Shout[]) => {
|
||||
const newArticleEntities = articles.reduce((acc, article) => {
|
||||
const addArticles = (...args: Shout[][]) => {
|
||||
const allArticles = args.flatMap((articles) => articles || [])
|
||||
|
||||
const newArticleEntities = allArticles.reduce((acc, article) => {
|
||||
acc[article.slug] = article
|
||||
return acc
|
||||
}, {} as Record<string, Shout>)
|
||||
|
@ -34,75 +80,121 @@ const addArticles = (articles: Shout[]) => {
|
|||
})
|
||||
}
|
||||
|
||||
if (!sortedArticlesStore) {
|
||||
sortedArticlesStore = atom<Shout[]>(articles)
|
||||
} else {
|
||||
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
|
||||
}
|
||||
const authorsByTopic = allArticles.reduce((acc, article) => {
|
||||
const { authors, topics } = article
|
||||
|
||||
const groupedByAuthors: Record<string, Shout[]> = {}
|
||||
const groupedByTopics: Record<string, Shout[]> = {}
|
||||
if (!articlesByTopicsStore || !articlesByAuthorsStore) {
|
||||
articles.forEach((a) => {
|
||||
a.authors.forEach((author) => {
|
||||
if (!groupedByAuthors[author.slug]) groupedByAuthors[author.slug] = []
|
||||
groupedByAuthors[author.slug].push(a)
|
||||
})
|
||||
a.topics.forEach((t) => {
|
||||
if (!groupedByTopics[t.slug]) groupedByTopics[t.slug] = []
|
||||
groupedByTopics[t.slug].push(a)
|
||||
topics.forEach((topic) => {
|
||||
if (!acc[topic.slug]) {
|
||||
acc[topic.slug] = []
|
||||
}
|
||||
|
||||
authors.forEach((author) => {
|
||||
if (!acc[topic.slug].some((a) => a.slug === author.slug)) {
|
||||
acc[topic.slug].push(author)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as { [topicSlug: string]: Author[] })
|
||||
|
||||
addAuthorsByTopic(authorsByTopic)
|
||||
|
||||
const topicsByAuthor = allArticles.reduce((acc, article) => {
|
||||
const { authors, topics } = article
|
||||
|
||||
authors.forEach((author) => {
|
||||
if (!acc[author.slug]) {
|
||||
acc[author.slug] = []
|
||||
}
|
||||
|
||||
topics.forEach((topic) => {
|
||||
if (!acc[author.slug].some((t) => t.slug === topic.slug)) {
|
||||
acc[author.slug].push(topic)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {} as { [authorSlug: string]: Topic[] })
|
||||
|
||||
addTopicsByAuthor(topicsByAuthor)
|
||||
}
|
||||
|
||||
const addSortedArticles = (articles: Shout[]) => {
|
||||
if (!sortedArticlesStore) {
|
||||
sortedArticlesStore = atom(articles)
|
||||
return
|
||||
}
|
||||
|
||||
if (!articlesByTopicsStore) {
|
||||
articlesByTopicsStore = atom<Record<string, Shout[]>>(groupedByTopics)
|
||||
} else {
|
||||
// TODO: deep update logix needed here
|
||||
articlesByTopicsStore.set({ ...articlesByTopicsStore.get(), ...groupedByTopics })
|
||||
}
|
||||
|
||||
if (!articlesByAuthorsStore) {
|
||||
articlesByAuthorsStore = atom<Record<string, Shout[]>>(groupedByAuthors)
|
||||
} else {
|
||||
// TODO: deep update logix needed here too
|
||||
articlesByAuthorsStore.set({ ...articlesByAuthorsStore.get(), ...groupedByAuthors })
|
||||
if (articles) {
|
||||
sortedArticlesStore.set([...sortedArticlesStore.get(), ...articles])
|
||||
}
|
||||
}
|
||||
|
||||
export const loadRecentAllArticles = async ({ page }: { page: number }): Promise<void> => {
|
||||
const newArticles = await apiClient.getRecentAllArticles({ page, size: 50 })
|
||||
export const loadRecentArticles = async ({ page }: { page: number }): Promise<void> => {
|
||||
const newArticles = await apiClient.getRecentArticles({ page })
|
||||
addArticles(newArticles)
|
||||
addSortedArticles(newArticles)
|
||||
}
|
||||
|
||||
export const loadRecentPublishedArticles = async ({ page }: { page: number }): Promise<void> => {
|
||||
const newArticles = await apiClient.getRecentPublishedArticles({ page, size: 50 })
|
||||
export const loadPublishedArticles = async ({ page }: { page: number }): Promise<void> => {
|
||||
const newArticles = await apiClient.getPublishedArticles({ page })
|
||||
addArticles(newArticles)
|
||||
addSortedArticles(newArticles)
|
||||
}
|
||||
|
||||
export const loadSearchResults = async ({ query }: { query: string }): Promise<void> => {
|
||||
const newArticles = await apiClient.getSearchResults({ query })
|
||||
addArticles(newArticles)
|
||||
addSortedArticles(newArticles)
|
||||
}
|
||||
|
||||
type InitialState = {
|
||||
sortedArticles?: Shout[]
|
||||
topRatedArticles?: Shout[]
|
||||
topRatedMonthArticles?: Shout[]
|
||||
}
|
||||
|
||||
export const useArticlesStore = ({ sortedArticles }: InitialState) => {
|
||||
addArticles(sortedArticles)
|
||||
export const useArticlesStore = ({
|
||||
sortedArticles,
|
||||
topRatedArticles,
|
||||
topRatedMonthArticles
|
||||
}: InitialState = {}) => {
|
||||
addArticles(sortedArticles, topRatedArticles, topRatedMonthArticles)
|
||||
addSortedArticles(sortedArticles)
|
||||
|
||||
if (!topRatedArticlesStore) {
|
||||
topRatedArticlesStore = atom(topRatedArticles)
|
||||
} else {
|
||||
topRatedArticlesStore.set(topRatedArticles)
|
||||
}
|
||||
|
||||
if (!topRatedMonthArticlesStore) {
|
||||
topRatedMonthArticlesStore = atom(topRatedMonthArticles)
|
||||
} else {
|
||||
topRatedMonthArticlesStore.set(topRatedMonthArticles)
|
||||
}
|
||||
|
||||
const getArticleEntities = useStore(articleEntitiesStore)
|
||||
const getSortedArticles = useStore(sortedArticlesStore)
|
||||
const getArticlesByAuthors = useStore(articlesByAuthorsStore)
|
||||
const getArticlesByTopics = useStore(articlesByTopicsStore)
|
||||
const getTopRatedArticles = useStore(topRatedArticlesStore)
|
||||
const getTopRatedMonthArticles = useStore(topRatedMonthArticlesStore)
|
||||
const getArticlesByAuthor = useStore(articlesByAuthorsStore)
|
||||
const getArticlesByTopic = useStore(articlesByTopicsStore)
|
||||
// TODO: get from server
|
||||
const getTopViewedArticles = useStore(topViewedArticlesStore)
|
||||
// TODO: get from server
|
||||
const getTopCommentedArticles = useStore(topCommentedArticlesStore)
|
||||
|
||||
return { getArticleEntities, getSortedArticles, getArticlesByTopics, getArticlesByAuthors }
|
||||
}
|
||||
|
||||
export const loadMoreAll = () => {
|
||||
const searchParams = useStore(params)
|
||||
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1
|
||||
loadRecentAllArticles({ page: pn + 1 })
|
||||
}
|
||||
|
||||
export const loadMorePublished = () => {
|
||||
const searchParams = useStore(params)
|
||||
const pn = Number.parseInt(searchParams()['page'] || '1', 10) || 1
|
||||
loadRecentPublishedArticles({ page: pn + 1 })
|
||||
return {
|
||||
getArticleEntities,
|
||||
getSortedArticles,
|
||||
getArticlesByTopic,
|
||||
getArticlesByAuthor,
|
||||
getTopRatedArticles,
|
||||
getTopViewedArticles,
|
||||
getTopCommentedArticles,
|
||||
getTopRatedMonthArticles
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,23 @@ import type { ReadableAtom, WritableAtom } from 'nanostores'
|
|||
import { atom, computed } from 'nanostores'
|
||||
import type { Author } from '../../graphql/types.gen'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { byCreated } from '../../utils/sortby'
|
||||
import { byCreated, byStat } from '../../utils/sortby'
|
||||
|
||||
export type AuthorsSortBy = 'created' | 'name'
|
||||
|
||||
const sortByStore = atom<AuthorsSortBy>('created')
|
||||
|
||||
let authorEntitiesStore: WritableAtom<Record<string, Author>>
|
||||
let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }>
|
||||
let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }>
|
||||
let sortedAuthorsStore: ReadableAtom<Author[]>
|
||||
let authorsByTopicStore: WritableAtom<Record<string, Author[]>>
|
||||
const initStore = (initial?: Record<string, Author>) => {
|
||||
let topAuthorsStore: ReadableAtom<Author[]>
|
||||
|
||||
const initStore = (initial: { [authorSlug: string]: Author }) => {
|
||||
if (authorEntitiesStore) {
|
||||
return
|
||||
}
|
||||
|
||||
authorEntitiesStore = atom<Record<string, Author>>(initial)
|
||||
authorEntitiesStore = atom(initial)
|
||||
|
||||
sortedAuthorsStore = computed([authorEntitiesStore, sortByStore], (authorEntities, sortBy) => {
|
||||
const authors = Object.values(authorEntities)
|
||||
|
@ -27,12 +29,17 @@ const initStore = (initial?: Record<string, Author>) => {
|
|||
break
|
||||
}
|
||||
case 'name': {
|
||||
// FIXME
|
||||
authors.sort((a, b) => a.name.localeCompare(b.name))
|
||||
break
|
||||
}
|
||||
}
|
||||
return authors
|
||||
})
|
||||
|
||||
topAuthorsStore = computed(authorEntitiesStore, (authorEntities) => {
|
||||
// TODO real top authors
|
||||
return Object.values(authorEntities)
|
||||
})
|
||||
}
|
||||
|
||||
const addAuthors = (authors: Author[]) => {
|
||||
|
@ -51,16 +58,47 @@ const addAuthors = (authors: Author[]) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[] }) => {
|
||||
const allAuthors = Object.values(authorsByTopic).flat()
|
||||
addAuthors(allAuthors)
|
||||
|
||||
if (!authorsByTopicStore) {
|
||||
authorsByTopicStore = atom<{ [topicSlug: string]: Author[] }>(authorsByTopic)
|
||||
} else {
|
||||
const newState = Object.entries(authorsByTopic).reduce((acc, [topicSlug, authors]) => {
|
||||
if (!acc[topicSlug]) {
|
||||
acc[topicSlug] = []
|
||||
}
|
||||
|
||||
authors.forEach((author) => {
|
||||
if (!acc[topicSlug].some((a) => a.slug === author.slug)) {
|
||||
acc[topicSlug].push(author)
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, authorsByTopicStore.get())
|
||||
|
||||
authorsByTopicStore.set(newState)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadAllAuthors = async (): Promise<void> => {
|
||||
const authors = await apiClient.getAllAuthors()
|
||||
addAuthors(authors)
|
||||
}
|
||||
|
||||
export const useAuthorsStore = (initial?: Author[]) => {
|
||||
if (initial) addAuthors(initial)
|
||||
type InitialState = {
|
||||
authors?: Author[]
|
||||
}
|
||||
|
||||
export const useAuthorsStore = ({ authors }: InitialState = {}) => {
|
||||
addAuthors(authors || [])
|
||||
|
||||
const getAuthorEntities = useStore(authorEntitiesStore)
|
||||
const getSortedAuthors = useStore(sortedAuthorsStore)
|
||||
const getAuthorsByTopic = useStore(authorsByTopicStore)
|
||||
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic }
|
||||
const getTopAuthors = useStore(topAuthorsStore)
|
||||
|
||||
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic, getTopAuthors }
|
||||
}
|
||||
|
|
|
@ -4,12 +4,19 @@ import { useStore } from '@nanostores/solid'
|
|||
|
||||
let currentArticleStore: WritableAtom<Shout | null>
|
||||
|
||||
// TODO add author, topic?
|
||||
export const useCurrentArticleStore = (initialState: Shout) => {
|
||||
type InitialState = {
|
||||
currentArticle: Shout
|
||||
}
|
||||
|
||||
export const useCurrentArticleStore = ({ currentArticle }: InitialState) => {
|
||||
if (!currentArticleStore) {
|
||||
currentArticleStore = atom(initialState)
|
||||
currentArticleStore = atom(currentArticle)
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// addTopicsByAuthor
|
||||
// addAuthorsByTopic
|
||||
|
||||
const getCurrentArticle = useStore(currentArticleStore)
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { action, atom, WritableAtom } from 'nanostores'
|
||||
import { atom, WritableAtom } from 'nanostores'
|
||||
import type { Reaction } from '../../graphql/types.gen'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import { reduceBy } from '../../utils/reduce'
|
||||
|
||||
export let reactionsOrdered: WritableAtom<Reaction[]>
|
||||
export const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
|
||||
// FIXME
|
||||
|
||||
let reactionsOrdered: WritableAtom<Reaction[]>
|
||||
const reactions = atom<{ [slug: string]: Reaction[] }>({}) // by shout
|
||||
|
||||
export const useReactionsStore = (initial?: Reaction[]) => {
|
||||
if (!reactionsOrdered) {
|
||||
|
@ -41,18 +43,14 @@ export const loadReactions = async ({
|
|||
reactionsOrdered.set(reactions)
|
||||
}
|
||||
|
||||
export const createReaction = async (reaction: Reaction) =>
|
||||
// FIXME
|
||||
reactionsOrdered.set(await apiClient.createReaction({ reaction }))
|
||||
|
||||
export const createReaction = (reaction) =>
|
||||
action(reactionsOrdered, 'createReaction', async (store) => {
|
||||
store.set(await apiClient.createReaction({ reaction }))
|
||||
})
|
||||
export const updateReaction = async (reaction: Reaction) =>
|
||||
// FIXME
|
||||
reactionsOrdered.set(await apiClient.updateReaction({ reaction }))
|
||||
|
||||
export const updateReaction = (reaction) =>
|
||||
action(reactionsOrdered, 'updateReaction', async (store) => {
|
||||
store.set(await apiClient.updateReaction({ reaction }))
|
||||
})
|
||||
|
||||
export const deleteReaction = (reaction_id) =>
|
||||
action(reactionsOrdered, 'deleteReaction', async (store) => {
|
||||
store.set(await apiClient.destroyReaction({ id: reaction_id }))
|
||||
})
|
||||
export const deleteReaction = async (reactionId: number) =>
|
||||
// FIXME
|
||||
reactionsOrdered.set(await apiClient.destroyReaction({ id: reactionId }))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { persistentAtom } from '@nanostores/persistent'
|
||||
import { action } from 'nanostores'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
|
||||
export const seen = persistentAtom<{ [slug: string]: Date }>(
|
||||
const seen = persistentAtom<{ [slug: string]: Date }>(
|
||||
'seen',
|
||||
{},
|
||||
{
|
||||
|
@ -10,9 +10,9 @@ export const seen = persistentAtom<{ [slug: string]: Date }>(
|
|||
}
|
||||
)
|
||||
|
||||
export const addSeen =
|
||||
(slug) => action(
|
||||
seen,
|
||||
'addSeen',
|
||||
(s) => s.set({ ...s.get(), [slug]: Date.now() })
|
||||
)
|
||||
export const addSeen = (slug) => seen.set({ ...seen.get(), [slug]: Date.now() })
|
||||
|
||||
export const useSeenStore = () => {
|
||||
const getSeen = useStore(seen)
|
||||
return { getSeen }
|
||||
}
|
||||
|
|
|
@ -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 type { Topic } from '../../graphql/types.gen'
|
||||
import { useStore } from '@nanostores/solid'
|
||||
import { byCreated } from '../../utils/sortby'
|
||||
import { byCreated, byStat } from '../../utils/sortby'
|
||||
|
||||
export type TopicsSortBy = 'created' | 'name'
|
||||
|
||||
const sortByStore = atom<TopicsSortBy>('created')
|
||||
|
||||
let topicEntitiesStore: WritableAtom<Record<string, Topic>>
|
||||
let topicEntitiesStore: WritableAtom<{ [topicSlug: string]: Topic }>
|
||||
let sortedTopicsStore: ReadableAtom<Topic[]>
|
||||
let topTopicsStore: ReadableAtom<Topic[]>
|
||||
let randomTopicsStore: WritableAtom<Topic[]>
|
||||
let topicsByAuthorStore: WritableAtom<{ [authorSlug: string]: Topic[] }>
|
||||
|
||||
const initStore = (initial?: Record<string, Topic>) => {
|
||||
if (topicEntitiesStore) {
|
||||
|
@ -35,10 +38,20 @@ const initStore = (initial?: Record<string, Topic>) => {
|
|||
}
|
||||
return topics
|
||||
})
|
||||
|
||||
topTopicsStore = computed(topicEntitiesStore, (topicEntities) => {
|
||||
const topics = Object.values(topicEntities)
|
||||
// DISCUSS
|
||||
// topics.sort(byStat('shouts'))
|
||||
topics.sort(byStat('rating'))
|
||||
return topics
|
||||
})
|
||||
}
|
||||
|
||||
const addTopics = (topics: Topic[] = []) => {
|
||||
const newTopicEntities = topics.reduce((acc, topic) => {
|
||||
const addTopics = (...args: Topic[][]) => {
|
||||
const allTopics = args.flatMap((topics) => topics || [])
|
||||
|
||||
const newTopicEntities = allTopics.reduce((acc, topic) => {
|
||||
acc[topic.slug] = topic
|
||||
return acc
|
||||
}, {} as Record<string, Topic>)
|
||||
|
@ -53,6 +66,31 @@ const addTopics = (topics: Topic[] = []) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic[] }) => {
|
||||
const allTopics = Object.values(topicsByAuthors).flat()
|
||||
addTopics(allTopics)
|
||||
|
||||
if (!topicsByAuthorStore) {
|
||||
topicsByAuthorStore = atom<{ [authorSlug: string]: Topic[] }>(topicsByAuthors)
|
||||
} else {
|
||||
const newState = Object.entries(topicsByAuthors).reduce((acc, [authorSlug, topics]) => {
|
||||
if (!acc[authorSlug]) {
|
||||
acc[authorSlug] = []
|
||||
}
|
||||
|
||||
topics.forEach((topic) => {
|
||||
if (!acc[authorSlug].some((t) => t.slug === topic.slug)) {
|
||||
acc[authorSlug].push(topic)
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, topicsByAuthorStore.get())
|
||||
|
||||
topicsByAuthorStore.set(newState)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadAllTopics = async (): Promise<void> => {
|
||||
const topics = await apiClient.getAllTopics()
|
||||
addTopics(topics)
|
||||
|
@ -63,11 +101,18 @@ type InitialState = {
|
|||
randomTopics?: Topic[]
|
||||
}
|
||||
|
||||
export const useTopicsStore = ({ topics }: InitialState) => {
|
||||
addTopics(topics)
|
||||
export const useTopicsStore = ({ topics, randomTopics }: InitialState = {}) => {
|
||||
addTopics(topics, randomTopics)
|
||||
|
||||
if (!randomTopicsStore) {
|
||||
randomTopicsStore = atom(randomTopics)
|
||||
}
|
||||
|
||||
const getTopicEntities = useStore(topicEntitiesStore)
|
||||
const getSortedTopics = useStore(sortedTopicsStore)
|
||||
const getRandomTopics = useStore(randomTopicsStore)
|
||||
const getTopicsByAuthor = useStore(topicsByAuthorStore)
|
||||
const getTopTopics = useStore(topTopicsStore)
|
||||
|
||||
return { getTopicEntities, getSortedTopics }
|
||||
return { getTopicEntities, getSortedTopics, getRandomTopics, getTopicsByAuthor, getTopTopics }
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import reactionsForShouts from '../graphql/query/reactions-for-shouts'
|
|||
import mySession from '../graphql/mutation/my-session'
|
||||
import { privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||
import authLogout from '../graphql/mutation/auth-logout'
|
||||
import authLogin from '../graphql/query/auth-login'
|
||||
import authRegister from '../graphql/mutation/auth-register'
|
||||
import followMutation from '../graphql/mutation/follow'
|
||||
import unfollowMutation from '../graphql/mutation/unfollow'
|
||||
|
@ -24,16 +25,21 @@ import authorsAll from '../graphql/query/authors-all'
|
|||
import reactionCreate from '../graphql/mutation/reaction-create'
|
||||
import reactionDestroy from '../graphql/mutation/reaction-destroy'
|
||||
import reactionUpdate from '../graphql/mutation/reaction-update'
|
||||
// import authorsBySlugs from '../graphql/query/authors-by-slugs'
|
||||
import authLogin from '../graphql/query/auth-login'
|
||||
import authCheck from '../graphql/query/auth-check'
|
||||
import authReset from '../graphql/mutation/auth-reset'
|
||||
import authForget from '../graphql/mutation/auth-forget'
|
||||
import authResend from '../graphql/mutation/auth-resend'
|
||||
import authorsBySlugs from '../graphql/query/authors-by-slugs'
|
||||
|
||||
const log = getLogger('api-client')
|
||||
const FEED_PAGE_SIZE = 50
|
||||
const REACTIONS_PAGE_SIZE = 100
|
||||
|
||||
const DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE = 50
|
||||
const DEFAULT_TOPIC_ARTICLES_PAGE_SIZE = 50
|
||||
const DEFAULT_RECENT_ARTICLES_PAGE_SIZE = 50
|
||||
const DEFAULT_REACTIONS_PAGE_SIZE = 50
|
||||
const DEFAULT_SEARCH_RESULTS_PAGE_SIZE = 50
|
||||
const DEFAULT_PUBLISHED_ARTICLES_PAGE_SIZE = 50
|
||||
const DEFAULT_RANDOM_TOPICS_AMOUNT = 12
|
||||
|
||||
export const apiClient = {
|
||||
getTopArticles: async () => {
|
||||
|
@ -44,15 +50,28 @@ export const apiClient = {
|
|||
const response = await publicGraphQLClient.query(articlesTopMonth, { page: 1, size: 10 }).toPromise()
|
||||
return response.data.topMonth
|
||||
},
|
||||
getRecentPublishedArticles: async ({
|
||||
page = 1,
|
||||
size = DEFAULT_RECENT_ARTICLES_PAGE_SIZE
|
||||
}: {
|
||||
page?: number
|
||||
size?: number
|
||||
}) => {
|
||||
const response = await publicGraphQLClient.query(articlesRecentPublished, { page, size }).toPromise()
|
||||
|
||||
return response.data.recentPublished
|
||||
},
|
||||
getRandomTopics: async () => {
|
||||
const response = await publicGraphQLClient.query(topicsRandomQuery, {}).toPromise()
|
||||
const response = await publicGraphQLClient
|
||||
.query(topicsRandomQuery, { amount: DEFAULT_RANDOM_TOPICS_AMOUNT })
|
||||
.toPromise()
|
||||
|
||||
return response.data.topicsRandom
|
||||
},
|
||||
getSearchResults: async ({
|
||||
query,
|
||||
page = 1,
|
||||
size = FEED_PAGE_SIZE
|
||||
size = DEFAULT_SEARCH_RESULTS_PAGE_SIZE
|
||||
}: {
|
||||
query: string
|
||||
page?: number
|
||||
|
@ -68,36 +87,26 @@ export const apiClient = {
|
|||
|
||||
return response.data.searchQuery
|
||||
},
|
||||
getRecentAllArticles: async ({ page, size }: { page?: number; size?: number }): Promise<Shout[]> => {
|
||||
const response = await publicGraphQLClient
|
||||
.query(articlesRecentAll, {
|
||||
page: page || 1,
|
||||
size: size || FEED_PAGE_SIZE
|
||||
})
|
||||
.toPromise()
|
||||
|
||||
return response.data.recentAll
|
||||
},
|
||||
getRecentPublishedArticles: async ({
|
||||
page,
|
||||
size
|
||||
getRecentArticles: async ({
|
||||
page = 1,
|
||||
size = DEFAULT_RECENT_ARTICLES_PAGE_SIZE
|
||||
}: {
|
||||
page?: number
|
||||
size?: number
|
||||
}): Promise<Shout[]> => {
|
||||
const response = await publicGraphQLClient
|
||||
.query(articlesRecentPublished, {
|
||||
page: page || 1,
|
||||
size: size || FEED_PAGE_SIZE
|
||||
.query(articlesRecentAll, {
|
||||
page,
|
||||
size
|
||||
})
|
||||
.toPromise()
|
||||
|
||||
return response.data.recentPublished
|
||||
return response.data.recentAll
|
||||
},
|
||||
getArticlesForTopics: async ({
|
||||
topicSlugs,
|
||||
page = 1,
|
||||
size = FEED_PAGE_SIZE
|
||||
size = DEFAULT_TOPIC_ARTICLES_PAGE_SIZE
|
||||
}: {
|
||||
topicSlugs: string[]
|
||||
page?: number
|
||||
|
@ -116,7 +125,7 @@ export const apiClient = {
|
|||
getArticlesForAuthors: async ({
|
||||
authorSlugs,
|
||||
page = 1,
|
||||
size = FEED_PAGE_SIZE
|
||||
size = DEFAULT_AUTHOR_ARTICLES_PAGE_SIZE
|
||||
}: {
|
||||
authorSlugs: string[]
|
||||
page?: number
|
||||
|
@ -183,15 +192,24 @@ export const apiClient = {
|
|||
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
||||
return response.data.refreshSession
|
||||
},
|
||||
getPublishedArticles: async ({
|
||||
page = 1,
|
||||
size = DEFAULT_PUBLISHED_ARTICLES_PAGE_SIZE
|
||||
}: {
|
||||
page?: number
|
||||
size?: number
|
||||
}) => {
|
||||
const response = await publicGraphQLClient.query(articlesRecentPublished, { page, size }).toPromise()
|
||||
|
||||
// feeds
|
||||
return response.data.recentPublished
|
||||
},
|
||||
getAllTopics: async () => {
|
||||
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
|
||||
return response.data.topicsAll
|
||||
},
|
||||
|
||||
getAllAuthors: async () => {
|
||||
const response = await publicGraphQLClient.query(authorsAll, { page: 1, size: 9999 }).toPromise()
|
||||
const response = await publicGraphQLClient.query(authorsAll, { page: 1, size: 999999 }).toPromise()
|
||||
return response.data.authorsAll
|
||||
},
|
||||
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
|
||||
|
@ -205,7 +223,7 @@ export const apiClient = {
|
|||
getReactionsForShouts: async ({
|
||||
shoutSlugs,
|
||||
page = 1,
|
||||
size = REACTIONS_PAGE_SIZE
|
||||
size = DEFAULT_REACTIONS_PAGE_SIZE
|
||||
}: {
|
||||
shoutSlugs: string[]
|
||||
page?: number
|
||||
|
@ -240,7 +258,10 @@ export const apiClient = {
|
|||
|
||||
return response.data.reactionsByShout
|
||||
},
|
||||
|
||||
getAuthorsBySlugs: async ({ slugs }) => {
|
||||
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise()
|
||||
return response.data.getUsersBySlugs
|
||||
},
|
||||
createReaction: async ({ reaction }) => {
|
||||
const response = await privateGraphQLClient.mutation(reactionCreate, { reaction }).toPromise()
|
||||
log.debug('[api] create reaction mutation called')
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import type { Stat } from '../graphql/types.gen'
|
||||
|
||||
export const byFirstChar = (a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || '')
|
||||
|
||||
export const byCreated = (a: any, b: any) => {
|
||||
|
@ -22,7 +24,7 @@ export const byLength = (a: any[], b: any[]) => {
|
|||
return 0
|
||||
}
|
||||
|
||||
export const byStat = (metric: string) => {
|
||||
export const byStat = (metric: keyof Stat) => {
|
||||
return (a, b) => {
|
||||
const x = (a?.stat && a.stat[metric]) || 0
|
||||
const y = (b?.stat && b.stat[metric]) || 0
|
||||
|
|
Loading…
Reference in New Issue
Block a user