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'