diff --git a/src/components/App.tsx b/src/components/App.tsx index 9fe54ab2..f6a22fe1 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -50,7 +50,6 @@ const pagesMap: Record> = { authorAbout: AuthorPage, inbox: InboxPage, expo: ExpoPage, - expoLayout: ExpoPage, connect: ConnectPage, create: CreatePage, edit: EditPage, diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 51c9a6bb..5f22dcf8 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -3,49 +3,89 @@ import { clsx } from 'clsx' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js' import { useLocalize } from '../../../context/localize' -import { LoadShoutsOptions, Shout } from '../../../graphql/types.gen' +import { LoadRandomTopShoutsParams, LoadShoutsOptions, Shout } from '../../../graphql/types.gen' import { LayoutType } from '../../../pages/types' -import { router, useRouter } from '../../../stores/router' +import { router } from '../../../stores/router' import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' +import { apiClient } from '../../../utils/apiClient' +import { getServerDate } from '../../../utils/getServerDate' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' import { splitToPages } from '../../../utils/splitToPages' import { Button } from '../../_shared/Button' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { Loading } from '../../_shared/Loading' +import { ArticleCardSwiper } from '../../_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCard } from '../../Feed/ArticleCard' import styles from './Expo.module.scss' type Props = { shouts: Shout[] + layout: LayoutType } -export const PRERENDERED_ARTICLES_COUNT = 28 + +export const PRERENDERED_ARTICLES_COUNT = 32 const LOAD_MORE_PAGE_SIZE = 16 + export const Expo = (props: Props) => { const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts)) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) + const [randomTopArticles, setRandomTopArticles] = createSignal([]) + const [randomTopMonthArticles, setRandomTopMonthArticles] = createSignal([]) + const { t } = useLocalize() - const { page: getPage } = useRouter() - const getLayout = createMemo(() => getPage().params['layout'] as LayoutType) + const { sortedArticles } = useArticlesStore({ shouts: isLoaded() ? props.shouts : [], }) - const loadMore = async (count) => { + const loadMore = async (count: number) => { saveScrollPosition() const options: LoadShoutsOptions = { limit: count, offset: sortedArticles().length, } - options.filters = getLayout() ? { layout: getLayout() } : { excludeLayout: 'article' } + options.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } const { hasMore } = await loadShouts(options) setIsLoadMoreButtonVisible(hasMore) restoreScrollPosition() } + const loadRandomTopArticles = async () => { + const params: LoadRandomTopShoutsParams = { + filters: { + visibility: 'public', + }, + limit: 10, + fromRandomCount: 100, + } + params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } + + const result = await apiClient.getRandomTopShouts(params) + setRandomTopArticles(result) + } + + const loadRandomTopMonthArticles = async () => { + const now = new Date() + const fromDate = getServerDate(new Date(now.setMonth(now.getMonth() - 1))) + + const params: LoadRandomTopShoutsParams = { + filters: { + visibility: 'public', + fromDate, + }, + limit: 10, + fromRandomCount: 10, + } + params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } + + const result = await apiClient.getRandomTopShouts(params) + setRandomTopMonthArticles(result) + } + const pages = createMemo(() => splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE), ) @@ -65,12 +105,21 @@ export const Expo = (props: Props) => { } }) + onMount(() => { + loadRandomTopArticles() + loadRandomTopMonthArticles() + }) + createEffect( on( - () => getLayout(), + () => props.layout, () => { resetSortedArticles() + setRandomTopArticles([]) + setRandomTopMonthArticles([]) loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE) + loadRandomTopArticles() + loadRandomTopMonthArticles() }, { defer: true }, ), @@ -89,49 +138,49 @@ export const Expo = (props: Props) => { 0} fallback={}>
- + {(shout) => (
{
)}
+ 0} keyed={true}> + + + + {(shout) => ( +
+ +
+ )} +
+ 0} keyed={true}> + + {(page) => ( diff --git a/src/components/Views/Search.tsx b/src/components/Views/Search.tsx index f87909a1..d8d179a7 100644 --- a/src/components/Views/Search.tsx +++ b/src/components/Views/Search.tsx @@ -36,10 +36,7 @@ export const SearchView = (props: Props) => { const loadMore = async () => { saveScrollPosition() const { hasMore } = await loadShouts({ - filters: { - title: query(), - body: query(), - }, + filters: {}, offset: offset(), limit: LOAD_MORE_PAGE_SIZE, }) diff --git a/src/graphql/query/articles-load-random-top.ts b/src/graphql/query/articles-load-random-top.ts new file mode 100644 index 00000000..9ac58789 --- /dev/null +++ b/src/graphql/query/articles-load-random-top.ts @@ -0,0 +1,46 @@ +import { gql } from '@urql/core' + +export default gql` + query LoadRandomTopShoutsQuery($params: LoadRandomTopShoutsParams) { + loadRandomTopShouts(params: $params) { + id + title + lead + description + subtitle + slug + layout + cover + lead + # community + mainTopic + topics { + id + title + body + slug + stat { + shouts + authors + followers + } + } + authors { + id + name + slug + userpic + createdAt + bio + } + createdAt + publishedAt + stat { + viewed + reacted + rating + commented + } + } + } +` diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index fd7bb869..32fa65cf 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -116,14 +116,19 @@ export enum FollowingEntity { Topic = 'TOPIC', } +export type LoadRandomTopShoutsParams = { + filters?: InputMaybe + fromRandomCount?: InputMaybe + limit: Scalars['Int']['input'] +} + export type LoadShoutsFilters = { author?: InputMaybe - body?: InputMaybe - days?: InputMaybe excludeLayout?: InputMaybe + fromDate?: InputMaybe layout?: InputMaybe reacted?: InputMaybe - title?: InputMaybe + toDate?: InputMaybe topic?: InputMaybe visibility?: InputMaybe } @@ -367,6 +372,7 @@ export type Query = { loadMessagesBy: Result loadMySubscriptions?: Maybe loadNotifications: NotificationsQueryResult + loadRandomTopShouts: Array> loadReactionsBy: Array> loadRecipients: Result loadShout?: Maybe @@ -419,6 +425,10 @@ export type QueryLoadNotificationsArgs = { params: NotificationsQueryParams } +export type QueryLoadRandomTopShoutsArgs = { + params?: InputMaybe +} + export type QueryLoadReactionsByArgs = { by: ReactionBy limit?: InputMaybe diff --git a/src/pages/expo/expo.page.route.ts b/src/pages/expo/expo.page.route.ts index 8270f299..9e19ae4d 100644 --- a/src/pages/expo/expo.page.route.ts +++ b/src/pages/expo/expo.page.route.ts @@ -1,4 +1,5 @@ import { ROUTES } from '../../stores/router' import { getServerRoute } from '../../utils/getServerRoute' -export default getServerRoute(ROUTES.expo) +// yes, it's a hack +export default getServerRoute(ROUTES.expo.replace(':layout?', '*')) diff --git a/src/pages/expo/expo.page.server.ts b/src/pages/expo/expo.page.server.ts index 1f771a42..ce6b3b94 100644 --- a/src/pages/expo/expo.page.server.ts +++ b/src/pages/expo/expo.page.server.ts @@ -4,12 +4,16 @@ import type { PageProps } from '../types' import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Expo/Expo' import { apiClient } from '../../utils/apiClient' -export const onBeforeRender = async (_pageContext: PageContext) => { +export const onBeforeRender = async (pageContext: PageContext) => { + const { layout } = pageContext.routeParams + const expoShouts = await apiClient.getShouts({ - filters: { excludeLayout: 'article' }, + filters: layout ? { layout } : { excludeLayout: 'article' }, limit: PRERENDERED_ARTICLES_COUNT, }) + const pageProps: PageProps = { expoShouts, seo: { title: '' } } + return { pageContext: { pageProps, diff --git a/src/pages/expo/expo.page.tsx b/src/pages/expo/expo.page.tsx index 658e68e0..5ed9e2ec 100644 --- a/src/pages/expo/expo.page.tsx +++ b/src/pages/expo/expo.page.tsx @@ -37,7 +37,7 @@ export const ExpoPage = (props: PageProps) => { return ( - + ) } diff --git a/src/pages/expo/expoLayout.page.route.ts b/src/pages/expo/expoLayout.page.route.ts deleted file mode 100644 index afdbd8b6..00000000 --- a/src/pages/expo/expoLayout.page.route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { ROUTES } from '../../stores/router' -import { getServerRoute } from '../../utils/getServerRoute' - -export default getServerRoute(ROUTES.expoLayout) diff --git a/src/pages/expo/expoLayout.page.server.ts b/src/pages/expo/expoLayout.page.server.ts deleted file mode 100644 index fbdd5118..00000000 --- a/src/pages/expo/expoLayout.page.server.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { PageContext } from '../../renderer/types' -import type { PageProps } from '../types' - -import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Expo/Expo' -import { apiClient } from '../../utils/apiClient' - -export const onBeforeRender = async (pageContext: PageContext) => { - const { layout } = pageContext.routeParams - const expoShouts = await apiClient.getShouts({ - filters: { layout: layout }, - limit: PRERENDERED_ARTICLES_COUNT, - }) - - const pageProps: PageProps = { expoShouts, seo: { title: '' } } - - return { - pageContext: { - pageProps, - }, - } -} diff --git a/src/pages/search.page.server.ts b/src/pages/search.page.server.ts index 7c4f7115..a3cfc224 100644 --- a/src/pages/search.page.server.ts +++ b/src/pages/search.page.server.ts @@ -6,7 +6,7 @@ import { apiClient } from '../utils/apiClient' export const onBeforeRender = async (pageContext: PageContext) => { const { q } = pageContext.routeParams - const searchResults = await apiClient.getShouts({ filters: { title: q, body: q }, limit: 50 }) + const searchResults = await apiClient.getShouts({ filters: {}, limit: 50 }) const pageProps: PageProps = { searchResults, seo: { title: '' } } diff --git a/src/pages/search.page.tsx b/src/pages/search.page.tsx index d7a72e3a..84b0fe92 100644 --- a/src/pages/search.page.tsx +++ b/src/pages/search.page.tsx @@ -24,7 +24,7 @@ export const SearchPage = (props: PageProps) => { return } - await loadShouts({ filters: { title: q(), body: q() }, limit: 50, offset: 0 }) + await loadShouts({ filters: {}, limit: 50, offset: 0 }) setIsLoaded(true) }) diff --git a/src/stores/router.ts b/src/stores/router.ts index 23e98b9c..4803ed82 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -37,8 +37,7 @@ export const ROUTES = { projects: '/about/projects', termsOfUse: '/about/terms-of-use', thanks: '/about/thanks', - expo: '/expo', - expoLayout: '/expo/:layout', + expo: '/expo/:layout?', profileSettings: '/profile/settings', profileSecurity: '/profile/security', profileSubscriptions: '/profile/subscriptions', diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index 48f6e316..530b61bd 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -4,6 +4,7 @@ import { createLazyMemo } from '@solid-primitives/memo' import { createSignal } from 'solid-js' import { apiClient } from '../../utils/apiClient' +import { getServerDate } from '../../utils/getServerDate' import { byStat } from '../../utils/sortby' import { addAuthorsByTopic } from './authors' @@ -193,11 +194,13 @@ type InitialState = { const TOP_MONTH_ARTICLES_COUNT = 10 export const loadTopMonthArticles = async (): Promise => { + const now = new Date() + const fromDate = getServerDate(new Date(now.setMonth(now.getMonth() - 1))) + const articles = await apiClient.getShouts({ filters: { visibility: 'public', - // TODO: replace with from, to - days: 30, + fromDate, }, order_by: 'rating_stat', limit: TOP_MONTH_ARTICLES_COUNT, diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 5256ee95..d77e44bd 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -19,6 +19,7 @@ import type { NotificationsQueryParams, NotificationsQueryResult, MySubscriptionsQueryResult, + LoadRandomTopShoutsParams, } from '../graphql/types.gen' import createArticle from '../graphql/mutation/article-create' @@ -43,6 +44,8 @@ import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import shoutLoad from '../graphql/query/article-load' import shoutsLoadBy from '../graphql/query/articles-load-by' +import shoutsLoadRandomTop from '../graphql/query/articles-load-random-top' +import articlesLoadRandomTop from '../graphql/query/articles-load-random-top' import authCheckEmailQuery from '../graphql/query/auth-check-email' import authLoginQuery from '../graphql/query/auth-login' import authorBySlug from '../graphql/query/author-by-slug' @@ -350,6 +353,15 @@ export const apiClient = { return resp.data.loadShouts }, + getRandomTopShouts: async (params: LoadRandomTopShoutsParams): Promise => { + const resp = await publicGraphQLClient.query(articlesLoadRandomTop, { params }).toPromise() + if (resp.error) { + console.error(resp) + } + + return resp.data.loadRandomTopShouts + }, + getMyFeed: async (options: LoadShoutsOptions) => { const resp = await privateGraphQLClient.query(myFeed, { options }).toPromise() diff --git a/src/utils/getServerDate.ts b/src/utils/getServerDate.ts new file mode 100644 index 00000000..feea507c --- /dev/null +++ b/src/utils/getServerDate.ts @@ -0,0 +1,4 @@ +export const getServerDate = (date: Date): string => { + // 2023-12-31 + return date.toISOString().slice(0, 10) +}