diff --git a/src/components/Pages/ArtworksPage.tsx b/src/components/Pages/ArtworksPage.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/Pages/AudioPage.tsx b/src/components/Pages/AudioPage.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/Pages/ReadingPage.tsx b/src/components/Pages/ReadingPage.tsx
new file mode 100644
index 00000000..15cbe4c2
--- /dev/null
+++ b/src/components/Pages/ReadingPage.tsx
@@ -0,0 +1,48 @@
+import { MainLayout } from '../Layouts/MainLayout'
+import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic'
+import type { PageProps } from '../types'
+import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
+import { resetSortedArticles } from '../../stores/zine/articles'
+import { useRouter } from '../../stores/router'
+import { loadLayoutShouts, loadLayout } from '../../stores/zine/layouts'
+import { Loading } from '../Loading'
+
+export const ReadingPage = (props: PageProps) => {
+ const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author))
+
+ const slug = createMemo(() => {
+ const { page: getPage } = useRouter()
+
+ const page = getPage()
+
+ if (page.route !== 'topic') {
+ throw new Error('ts guard')
+ }
+
+ return page.params.slug
+ })
+
+ onMount(async () => {
+ if (isLoaded()) {
+ return
+ }
+
+ await loadLayoutShouts({ topicSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
+ await loadLayout({ slug: slug() })
+
+ setIsLoaded(true)
+ })
+
+ onCleanup(() => resetSortedArticles())
+
+ return (
+
+ }>
+
+
+
+ )
+}
+
+// for lazy loading
+export default TopicPage
diff --git a/src/components/Pages/VIdeoPage.tsx b/src/components/Pages/VIdeoPage.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/Root.tsx b/src/components/Root.tsx
index fc1a7ff9..ce16d897 100644
--- a/src/components/Root.tsx
+++ b/src/components/Root.tsx
@@ -31,26 +31,13 @@ import { ThanksPage } from './Pages/about/ThanksPage'
import { CreatePage } from './Pages/CreatePage'
import { ConnectPage } from './Pages/ConnectPage'
import { renewSession } from '../stores/auth'
+import { AudioPage } from './Pages/AudioPage'
+import { VideoPage } from './Pages/VideoPage'
+import { ReadingPage } from './Pages/ReadingPage'
+import { ArtworksPage } from './Pages/ArtworksPage'
// TODO: lazy load
-// const HomePage = lazy(() => import('./Pages/HomePage'))
-// const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage'))
-// const TopicPage = lazy(() => import('./Pages/TopicPage'))
-// const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage'))
-// const AuthorPage = lazy(() => import('./Pages/AuthorPage'))
-// const FeedPage = lazy(() => import('./Pages/FeedPage'))
-// const ArticlePage = lazy(() => import('./Pages/ArticlePage'))
-// const SearchPage = lazy(() => import('./Pages/SearchPage'))
-// const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage'))
-// const DogmaPage = lazy(() => import('./Pages/about/DogmaPage'))
-// const GuidePage = lazy(() => import('./Pages/about/GuidePage'))
-// const HelpPage = lazy(() => import('./Pages/about/HelpPage'))
-// const ManifestPage = lazy(() => import('./Pages/about/ManifestPage'))
-// const PartnersPage = lazy(() => import('./Pages/about/PartnersPage'))
-// const ProjectsPage = lazy(() => import('./Pages/about/ProjectsPage'))
-// const TermsOfUsePage = lazy(() => import('./Pages/about/TermsOfUsePage'))
-// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
-// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
+// const SomePage = lazy(() => import('./Pages/SomePage'))
const log = getLogger('root')
@@ -60,6 +47,10 @@ type RootSearchParams = {
}
const pagesMap: Record> = {
+ audio: AudioPage,
+ video: VideoPage,
+ literature: ReadingPage,
+ artworks: ArtworksPage,
connect: ConnectPage,
create: CreatePage,
home: HomePage,
diff --git a/src/graphql/query/articles-for-layout.ts b/src/graphql/query/articles-for-layout.ts
new file mode 100644
index 00000000..f5ea3c13
--- /dev/null
+++ b/src/graphql/query/articles-for-layout.ts
@@ -0,0 +1,40 @@
+import { gql } from '@urql/core'
+
+export default gql`
+ query ShoutsForLayoutQuery($amount: Int, $offset: Int, $layout: String) {
+ shoutsByLayout(amount: $amount, offset: $offset, layout: $layout) {
+ _id: slug
+ title
+ subtitle
+ layout
+ slug
+ cover
+ # community
+ mainTopic
+ topics {
+ title
+ body
+ slug
+ stat {
+ _id: shouts
+ shouts
+ authors
+ followers
+ }
+ }
+ authors {
+ _id: slug
+ name
+ slug
+ userpic
+ }
+ createdAt
+ publishedAt
+ stat {
+ _id: viewed
+ viewed
+ reacted
+ }
+ }
+ }
+`
diff --git a/src/pages/layout/[...layout].astro b/src/pages/layout/[...layout].astro
new file mode 100644
index 00000000..2615cd26
--- /dev/null
+++ b/src/pages/layout/[...layout].astro
@@ -0,0 +1,23 @@
+---
+import { Root } from '../../components/Root'
+import Zine from '../../layouts/zine.astro'
+import { apiClient } from '../../utils/apiClient'
+import { initRouter } from '../../stores/router'
+
+const layout = Astro.params.layout?.toString()
+if (layout.endsWith('.map')) {
+ return Astro.redirect('/404')
+}
+const LAYOUTS = ['literature', 'audio', 'video', 'artworks']
+if (!LAYOUTS.includes(layout)) {
+ return Astro.redirect('/404')
+}
+const shouts = await apiClient.getRecentByLayout(layout)
+const { pathname, search } = Astro.url
+initRouter(pathname, search)
+---
+
+
+
+
+
diff --git a/src/stores/zine/layouts.ts b/src/stores/zine/layouts.ts
new file mode 100644
index 00000000..1e0d77bf
--- /dev/null
+++ b/src/stores/zine/layouts.ts
@@ -0,0 +1,196 @@
+import type { Author, Shout, ShoutInput, Topic } from '../../graphql/types.gen'
+import { apiClient } from '../../utils/apiClient'
+import { addAuthorsByTopic } from './authors'
+import { addTopicsByAuthor } from './topics'
+import { byStat } from '../../utils/sortby'
+import { createSignal } from 'solid-js'
+import { createLazyMemo } from '@solid-primitives/memo'
+
+const [sortedArticles, setSortedArticles] = createSignal([])
+const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
+
+const [topArticles, setTopArticles] = createSignal([])
+const [topMonthArticles, setTopMonthArticles] = createSignal([])
+
+const articlesByLayout = createLazyMemo(() => {
+ return Object.values(articleEntities()).reduce((acc, article) => {
+ if (!acc[article.layout]) {
+ acc[article.layout] = []
+ }
+
+ acc[article.layout].push(article)
+
+ return acc
+ }, {} as { [layout: string]: Shout[] })
+})
+
+const topViewedArticles = createLazyMemo(() => {
+ const result = Object.values(articleEntities())
+ result.sort(byStat('viewed'))
+ return result
+})
+
+const topCommentedArticles = createLazyMemo(() => {
+ const result = Object.values(articleEntities())
+ result.sort(byStat('commented'))
+ return result
+})
+
+// eslint-disable-next-line sonarjs/cognitive-complexity
+const addArticles = (...args: Shout[][]) => {
+ const allArticles = args.flatMap((articles) => articles || [])
+
+ const newArticleEntities = allArticles.reduce((acc, article) => {
+ acc[article.slug] = article
+ return acc
+ }, {} as { [articleSLug: string]: Shout })
+
+ setArticleEntities((prevArticleEntities) => {
+ return {
+ ...prevArticleEntities,
+ ...newArticleEntities
+ }
+ })
+
+ const authorsByTopic = allArticles.reduce((acc, article) => {
+ const { authors, topics } = article
+
+ topics.forEach((topic) => {
+ if (!acc[topic.slug]) {
+ acc[topic.slug] = []
+ }
+
+ authors.forEach((author) => {
+ if (!acc[topic.slug].some((a) => a.slug === author.slug)) {
+ acc[topic.slug].push(author)
+ }
+ })
+ })
+
+ return acc
+ }, {} as { [topicSlug: string]: Author[] })
+
+ addAuthorsByTopic(authorsByTopic)
+
+ const topicsByAuthor = allArticles.reduce((acc, article) => {
+ const { authors, topics } = article
+
+ authors.forEach((author) => {
+ if (!acc[author.slug]) {
+ acc[author.slug] = []
+ }
+
+ topics.forEach((topic) => {
+ if (!acc[author.slug].some((t) => t.slug === topic.slug)) {
+ acc[author.slug].push(topic)
+ }
+ })
+ })
+
+ return acc
+ }, {} as { [authorSlug: string]: Topic[] })
+
+ addTopicsByAuthor(topicsByAuthor)
+}
+
+const addSortedArticles = (articles: Shout[]) => {
+ setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
+}
+
+export const loadLayoutShouts = async ({
+ layout,
+ limit,
+ offset
+}: {
+ layout: string
+ limit: number
+ offset?: number
+}): Promise<{ hasMore: boolean }> => {
+ const newArticles = await apiClient.getLayoutShouts({ layout, limit: limit + 1, offset })
+ const hasMore = newArticles.length === limit + 1
+
+ if (hasMore) {
+ newArticles.splice(-1)
+ }
+
+ addArticles(newArticles)
+ addSortedArticles(newArticles)
+
+ return { hasMore }
+}
+
+export const resetSortedArticles = () => {
+ setSortedArticles([])
+}
+
+export const loadTopMonthArticles = async (): Promise => {
+ const articles = await apiClient.getTopMonthArticles()
+ addArticles(articles)
+ setTopMonthArticles(articles)
+}
+
+export const loadTopArticles = async (): Promise => {
+ const articles = await apiClient.getTopArticles()
+ addArticles(articles)
+ setTopArticles(articles)
+}
+
+export const loadSearchResults = async ({
+ query,
+ limit,
+ offset
+}: {
+ query: string
+ limit?: number
+ offset?: number
+}): Promise => {
+ const newArticles = await apiClient.getSearchResults({ query, limit, offset })
+ addArticles(newArticles)
+ addSortedArticles(newArticles)
+}
+
+export const incrementView = async ({ articleSlug }: { articleSlug: string }): Promise => {
+ await apiClient.incrementView({ articleSlug })
+}
+
+export const loadArticle = async ({ slug }: { slug: string }): Promise => {
+ const article = await apiClient.getArticle({ slug })
+
+ if (!article) {
+ throw new Error(`Can't load article, slug: "${slug}"`)
+ }
+
+ addArticles([article])
+}
+
+export const createArticle = async ({ article }: { article: ShoutInput }) => {
+ try {
+ await apiClient.createArticle({ article })
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+type InitialState = {
+ sortedArticles?: Shout[]
+ topRatedArticles?: Shout[]
+ topRatedMonthArticles?: Shout[]
+}
+
+export const useArticlesStore = (initialState: InitialState = {}) => {
+ addArticles([...(initialState.sortedArticles || [])])
+
+ if (initialState.sortedArticles) {
+ setSortedArticles([...initialState.sortedArticles])
+ }
+
+ return {
+ articleEntities,
+ sortedArticles,
+ topArticles,
+ topMonthArticles,
+ topViewedArticles,
+ topCommentedArticles,
+ articlesByLayout
+ }
+}
diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts
index d798488e..f2792a16 100644
--- a/src/utils/apiClient.ts
+++ b/src/utils/apiClient.ts
@@ -29,6 +29,7 @@ import authorsBySlugs from '../graphql/query/authors-by-slugs'
import incrementView from '../graphql/mutation/increment-view'
import createArticle from '../graphql/mutation/article-create'
import myChats from '../graphql/query/my-chats'
+import getLayout from '../graphql/query/articles-for-layout'
const FEED_SIZE = 50
@@ -334,5 +335,8 @@ export const apiClient = {
getInboxes: async (payload = {}) => {
const resp = await privateGraphQLClient.query(myChats, payload).toPromise()
return resp.data.myChats
+ },
+ getLayoutShouts: async (layout = 'article', amount = 50, offset = 0) => {
+ const resp = await publicGraphQLClient.query(getLayout, { amount, offset, layout })
}
}
diff --git a/src/utils/config.ts b/src/utils/config.ts
index ab0ab70b..97487fb0 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -1,4 +1,5 @@
export const isDev = import.meta.env.MODE === 'development'
-export const apiBaseUrl = 'https://newapi.discours.io'
+// export const apiBaseUrl = 'https://newapi.discours.io'
+export const apiBaseUrl = 'https://testapi.discours.io'
// export const apiBaseUrl = 'http://localhost:8080'