diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index ee37c254..6b7a4173 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -41,6 +41,7 @@ "Back": "Back", "Back to editor": "Back to editor", "Back to main page": "Back to main page", + "Be the first to rate": "Be the first to rate", "Become an author": "Become an author", "Bold": "Bold", "Bookmarked": "Saved", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 560ff2b6..5f9fd460 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -44,6 +44,7 @@ "Back": "Назад", "Back to editor": "Вернуться в редактор", "Back to main page": "Вернуться на главную", + "Be the first to rate": "Оцените первым", "Become an author": "Стать автором", "Bold": "Жирный", "Bookmarked": "Сохранено", diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 5bf55248..00654f34 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -46,7 +46,7 @@ export type ArticleCardProps = { withViewed?: boolean noAuthorLink?: boolean } - desktopCoverSize: 'XS' | 'S' | 'M' | 'L' + desktopCoverSize?: 'XS' | 'S' | 'M' | 'L' article: Shout } diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 5f22dcf8..06c592e2 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -3,7 +3,12 @@ import { clsx } from 'clsx' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js' import { useLocalize } from '../../../context/localize' -import { LoadRandomTopShoutsParams, LoadShoutsOptions, Shout } from '../../../graphql/types.gen' +import { + LoadRandomTopShoutsParams, + LoadShoutsFilters, + LoadShoutsOptions, + Shout, +} from '../../../graphql/types.gen' import { LayoutType } from '../../../pages/types' import { router } from '../../../stores/router' import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' @@ -24,7 +29,7 @@ type Props = { layout: LayoutType } -export const PRERENDERED_ARTICLES_COUNT = 32 +export const PRERENDERED_ARTICLES_COUNT = 24 const LOAD_MORE_PAGE_SIZE = 16 export const Expo = (props: Props) => { @@ -40,15 +45,26 @@ export const Expo = (props: Props) => { shouts: isLoaded() ? props.shouts : [], }) + const getLoadShoutsFilters = (filters: LoadShoutsFilters = {}): LoadShoutsFilters => { + const result = { ...filters } + + if (props.layout) { + filters.layout = props.layout + } else { + filters.excludeLayout = 'article' + } + + return result + } + const loadMore = async (count: number) => { saveScrollPosition() const options: LoadShoutsOptions = { + filters: getLoadShoutsFilters(), limit: count, offset: sortedArticles().length, } - options.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } - const { hasMore } = await loadShouts(options) setIsLoadMoreButtonVisible(hasMore) restoreScrollPosition() @@ -56,13 +72,10 @@ export const Expo = (props: Props) => { const loadRandomTopArticles = async () => { const params: LoadRandomTopShoutsParams = { - filters: { - visibility: 'public', - }, + filters: getLoadShoutsFilters(), limit: 10, fromRandomCount: 100, } - params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } const result = await apiClient.getRandomTopShouts(params) setRandomTopArticles(result) @@ -73,14 +86,10 @@ export const Expo = (props: Props) => { const fromDate = getServerDate(new Date(now.setMonth(now.getMonth() - 1))) const params: LoadRandomTopShoutsParams = { - filters: { - visibility: 'public', - fromDate, - }, + filters: getLoadShoutsFilters({ fromDate }), limit: 10, fromRandomCount: 10, } - params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' } const result = await apiClient.getRandomTopShouts(params) setRandomTopMonthArticles(result) @@ -103,9 +112,7 @@ export const Expo = (props: Props) => { if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { loadMore(LOAD_MORE_PAGE_SIZE) } - }) - onMount(() => { loadRandomTopArticles() loadRandomTopMonthArticles() }) diff --git a/src/components/Views/Feed.module.scss b/src/components/Views/Feed/Feed.module.scss similarity index 100% rename from src/components/Views/Feed.module.scss rename to src/components/Views/Feed/Feed.module.scss diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed/Feed.tsx similarity index 83% rename from src/components/Views/Feed.tsx rename to src/components/Views/Feed/Feed.tsx index 7a3869e9..a2253965 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -1,32 +1,32 @@ -import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../graphql/types.gen' +import type { Author, LoadShoutsOptions, Reaction, Shout } from '../../../graphql/types.gen' import { getPagePath } from '@nanostores/router' import { Meta } from '@solidjs/meta' import { clsx } from 'clsx' import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js' -import { useLocalize } from '../../context/localize' -import { useReactions } from '../../context/reactions' -import { router, useRouter } from '../../stores/router' -import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles' -import { useTopAuthorsStore } from '../../stores/zine/topAuthors' -import { useTopicsStore } from '../../stores/zine/topics' -import { capitalize } from '../../utils/capitalize' -import { getImageUrl } from '../../utils/getImageUrl' -import { getDescription } from '../../utils/meta' -import { Icon } from '../_shared/Icon' -import { Loading } from '../_shared/Loading' -import { CommentDate } from '../Article/CommentDate' -import { AuthorLink } from '../Author/AhtorLink' -import { AuthorBadge } from '../Author/AuthorBadge' -import { ArticleCard } from '../Feed/ArticleCard' -import { Sidebar } from '../Feed/Sidebar' +import { useLocalize } from '../../../context/localize' +import { useReactions } from '../../../context/reactions' +import { router, useRouter } from '../../../stores/router' +import { useArticlesStore, resetSortedArticles } from '../../../stores/zine/articles' +import { useTopAuthorsStore } from '../../../stores/zine/topAuthors' +import { useTopicsStore } from '../../../stores/zine/topics' +import { apiClient } from '../../../utils/apiClient' +import { getImageUrl } from '../../../utils/getImageUrl' +import { Icon } from '../../_shared/Icon' +import { Loading } from '../../_shared/Loading' +import { CommentDate } from '../../Article/CommentDate' +import { AuthorLink } from '../../Author/AhtorLink' +import { AuthorBadge } from '../../Author/AuthorBadge' +import { ArticleCard } from '../../Feed/ArticleCard' +import { Sidebar } from '../../Feed/Sidebar' import styles from './Feed.module.scss' -import stylesBeside from '../../components/Feed/Beside.module.scss' -import stylesTopic from '../Feed/CardTopic.module.scss' +import stylesBeside from '../../Feed/Beside.module.scss' +import stylesTopic from '../../Feed/CardTopic.module.scss' export const FEED_PAGE_SIZE = 20 +const UNRATED_ARTICLES_COUNT = 5 type FeedSearchParams = { by: 'publish_date' | 'rating' | 'last_comment' @@ -51,7 +51,7 @@ type Props = { }> } -export const FeedView = (props: Props) => { +export const Feed = (props: Props) => { const { t } = useLocalize() const { page, searchParams } = useRouter() const [isLoading, setIsLoading] = createSignal(false) @@ -62,13 +62,20 @@ export const FeedView = (props: Props) => { const { topAuthors } = useTopAuthorsStore() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [topComments, setTopComments] = createSignal([]) + const [unratedArticles, setUnratedArticles] = createSignal([]) const { actions: { loadReactionsBy }, } = useReactions() + const loadUnratedArticles = async () => { + const result = await apiClient.getUnratedShouts(UNRATED_ARTICLES_COUNT) + setUnratedArticles(result) + } + onMount(() => { loadMore() + loadUnratedArticles() }) createEffect( @@ -271,6 +278,14 @@ export const FeedView = (props: Props) => { + 0}> +
+

{t('Be the first to rate')}

+ + {(article) => } + +
+
diff --git a/src/components/Views/Feed/index.ts b/src/components/Views/Feed/index.ts new file mode 100644 index 00000000..f0fa7232 --- /dev/null +++ b/src/components/Views/Feed/index.ts @@ -0,0 +1 @@ +export { Feed } from './Feed' diff --git a/src/components/Views/Search.tsx b/src/components/Views/Search.tsx index d8d179a7..be54593b 100644 --- a/src/components/Views/Search.tsx +++ b/src/components/Views/Search.tsx @@ -29,7 +29,7 @@ export const SearchView = (props: Props) => { const { searchParams } = useRouter() let searchEl: HTMLInputElement - const handleQueryChange = (_ev) => { + const handleQueryChange = () => { setQuery(searchEl.value) } diff --git a/src/components/_shared/SolidSwiper/Swiper.module.scss b/src/components/_shared/SolidSwiper/Swiper.module.scss index 500efb7d..4928695e 100644 --- a/src/components/_shared/SolidSwiper/Swiper.module.scss +++ b/src/components/_shared/SolidSwiper/Swiper.module.scss @@ -101,7 +101,7 @@ padding: 0; & swiper-slide { - //bind to html element + // bind to html element width: unset !important; } diff --git a/src/graphql/query/articles-load-unrated.ts b/src/graphql/query/articles-load-unrated.ts new file mode 100644 index 00000000..36d00301 --- /dev/null +++ b/src/graphql/query/articles-load-unrated.ts @@ -0,0 +1,46 @@ +import { gql } from '@urql/core' + +export default gql` + query LoadUnratedShoutsQuery($limit: Int!) { + loadUnratedShouts(limit: $limit) { + 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 32fa65cf..609977af 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -377,6 +377,7 @@ export type Query = { loadRecipients: Result loadShout?: Maybe loadShouts: Array> + loadUnratedShouts: Array> markdownBody: Scalars['String']['output'] myFeed?: Maybe>> searchMessages: Result @@ -449,6 +450,10 @@ export type QueryLoadShoutsArgs = { options?: InputMaybe } +export type QueryLoadUnratedShoutsArgs = { + limit: Scalars['Int']['input'] +} + export type QueryMarkdownBodyArgs = { body: Scalars['String']['input'] } diff --git a/src/pages/author.page.tsx b/src/pages/author.page.tsx index 3be8caf9..a13e531f 100644 --- a/src/pages/author.page.tsx +++ b/src/pages/author.page.tsx @@ -5,7 +5,6 @@ import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } import { Loading } from '../components/_shared/Loading' import { PageLayout } from '../components/_shared/PageLayout' import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Author' -import { useLocalize } from '../context/localize' import { ReactionsProvider } from '../context/reactions' import { useRouter } from '../stores/router' import { loadShouts, resetSortedArticles } from '../stores/zine/articles' diff --git a/src/pages/expo/expo.page.tsx b/src/pages/expo/expo.page.tsx index 5ed9e2ec..d6bf64b0 100644 --- a/src/pages/expo/expo.page.tsx +++ b/src/pages/expo/expo.page.tsx @@ -1,6 +1,6 @@ import type { PageProps } from '../types' -import { createMemo } from 'solid-js' +import { createEffect, createMemo, on } from 'solid-js' import { PageLayout } from '../../components/_shared/PageLayout' import { Topics } from '../../components/Nav/Topics' @@ -14,7 +14,7 @@ export const ExpoPage = (props: PageProps) => { const { page } = useRouter() const getLayout = createMemo(() => page().params['layout'] as LayoutType) - const title = createMemo(() => { + const getTitle = () => { switch (getLayout()) { case 'music': { return t('Audio') @@ -32,10 +32,20 @@ export const ExpoPage = (props: PageProps) => { return t('Art') } } - }) + } + + createEffect( + on( + () => getLayout(), + () => { + document.title = getTitle() + }, + { defer: true }, + ), + ) return ( - + diff --git a/src/pages/feed.page.tsx b/src/pages/feed.page.tsx index ee3c8bf1..3daf019d 100644 --- a/src/pages/feed.page.tsx +++ b/src/pages/feed.page.tsx @@ -2,7 +2,7 @@ import { createEffect, Match, on, onCleanup, Switch } from 'solid-js' import { PageLayout } from '../components/_shared/PageLayout' import { AuthGuard } from '../components/AuthGuard' -import { FeedView } from '../components/Views/Feed' +import { Feed } from '../components/Views/Feed' import { useLocalize } from '../context/localize' import { ReactionsProvider } from '../context/reactions' import { LoadShoutsOptions } from '../graphql/types.gen' @@ -40,13 +40,13 @@ export const FeedPage = () => { return ( - }> + }> - + - + diff --git a/src/pages/topic.page.tsx b/src/pages/topic.page.tsx index 12e029ef..574eb12a 100644 --- a/src/pages/topic.page.tsx +++ b/src/pages/topic.page.tsx @@ -4,16 +4,13 @@ import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from ' import { PageLayout } from '../components/_shared/PageLayout' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic' -import { useLocalize } from '../context/localize' import { ReactionsProvider } from '../context/reactions' import { useRouter } from '../stores/router' import { loadShouts, resetSortedArticles } from '../stores/zine/articles' import { loadTopic } from '../stores/zine/topics' -import { capitalize } from '../utils/capitalize' export const TopicPage = (props: PageProps) => { const { page } = useRouter() - const { t } = useLocalize() const slug = createMemo(() => page().params['slug'] as string) const [isLoaded, setIsLoaded] = createSignal( diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index 530b61bd..e8fd9842 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -1,4 +1,4 @@ -import type { Author, Shout, ShoutInput, LoadShoutsOptions } from '../../graphql/types.gen' +import type { Author, Shout, LoadShoutsOptions } from '../../graphql/types.gen' import { createLazyMemo } from '@solid-primitives/memo' import { createSignal } from 'solid-js' @@ -179,14 +179,6 @@ export const resetSortedArticles = () => { setSortedArticles([]) } -export const createArticle = async ({ article }: { article: ShoutInput }) => { - try { - await apiClient.createArticle({ article }) - } catch (error) { - console.error(error) - } -} - type InitialState = { shouts?: Shout[] } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index d77e44bd..ee966de1 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -44,8 +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 articlesLoadUnrated from '../graphql/query/articles-load-unrated' import authCheckEmailQuery from '../graphql/query/auth-check-email' import authLoginQuery from '../graphql/query/auth-login' import authorBySlug from '../graphql/query/author-by-slug' @@ -362,6 +362,15 @@ export const apiClient = { return resp.data.loadRandomTopShouts }, + getUnratedShouts: async (limit: number): Promise => { + const resp = await publicGraphQLClient.query(articlesLoadUnrated, { limit }).toPromise() + if (resp.error) { + console.error(resp) + } + + return resp.data.loadUnratedShouts + }, + getMyFeed: async (options: LoadShoutsOptions) => { const resp = await privateGraphQLClient.query(myFeed, { options }).toPromise()