Feature/unrated (#334)

* Rate first markup

* WIP

* lint

* unrated articles + random top fixes

---------

Co-authored-by: ilya-bkv <i.yablokov@ccmp.me>
Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
Igor Lobanov 2023-12-14 19:45:50 +01:00 committed by GitHub
parent e5846deab7
commit d2977b9b21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 143 additions and 60 deletions

View File

@ -41,6 +41,7 @@
"Back": "Back", "Back": "Back",
"Back to editor": "Back to editor", "Back to editor": "Back to editor",
"Back to main page": "Back to main page", "Back to main page": "Back to main page",
"Be the first to rate": "Be the first to rate",
"Become an author": "Become an author", "Become an author": "Become an author",
"Bold": "Bold", "Bold": "Bold",
"Bookmarked": "Saved", "Bookmarked": "Saved",

View File

@ -44,6 +44,7 @@
"Back": "Назад", "Back": "Назад",
"Back to editor": "Вернуться в редактор", "Back to editor": "Вернуться в редактор",
"Back to main page": "Вернуться на главную", "Back to main page": "Вернуться на главную",
"Be the first to rate": "Оцените первым",
"Become an author": "Стать автором", "Become an author": "Стать автором",
"Bold": "Жирный", "Bold": "Жирный",
"Bookmarked": "Сохранено", "Bookmarked": "Сохранено",

View File

@ -46,7 +46,7 @@ export type ArticleCardProps = {
withViewed?: boolean withViewed?: boolean
noAuthorLink?: boolean noAuthorLink?: boolean
} }
desktopCoverSize: 'XS' | 'S' | 'M' | 'L' desktopCoverSize?: 'XS' | 'S' | 'M' | 'L'
article: Shout article: Shout
} }

View File

@ -3,7 +3,12 @@ import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' 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 { LayoutType } from '../../../pages/types'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles' import { loadShouts, resetSortedArticles, useArticlesStore } from '../../../stores/zine/articles'
@ -24,7 +29,7 @@ type Props = {
layout: LayoutType layout: LayoutType
} }
export const PRERENDERED_ARTICLES_COUNT = 32 export const PRERENDERED_ARTICLES_COUNT = 24
const LOAD_MORE_PAGE_SIZE = 16 const LOAD_MORE_PAGE_SIZE = 16
export const Expo = (props: Props) => { export const Expo = (props: Props) => {
@ -40,15 +45,26 @@ export const Expo = (props: Props) => {
shouts: isLoaded() ? props.shouts : [], 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) => { const loadMore = async (count: number) => {
saveScrollPosition() saveScrollPosition()
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: getLoadShoutsFilters(),
limit: count, limit: count,
offset: sortedArticles().length, offset: sortedArticles().length,
} }
options.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' }
const { hasMore } = await loadShouts(options) const { hasMore } = await loadShouts(options)
setIsLoadMoreButtonVisible(hasMore) setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition() restoreScrollPosition()
@ -56,13 +72,10 @@ export const Expo = (props: Props) => {
const loadRandomTopArticles = async () => { const loadRandomTopArticles = async () => {
const params: LoadRandomTopShoutsParams = { const params: LoadRandomTopShoutsParams = {
filters: { filters: getLoadShoutsFilters(),
visibility: 'public',
},
limit: 10, limit: 10,
fromRandomCount: 100, fromRandomCount: 100,
} }
params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' }
const result = await apiClient.getRandomTopShouts(params) const result = await apiClient.getRandomTopShouts(params)
setRandomTopArticles(result) setRandomTopArticles(result)
@ -73,14 +86,10 @@ export const Expo = (props: Props) => {
const fromDate = getServerDate(new Date(now.setMonth(now.getMonth() - 1))) const fromDate = getServerDate(new Date(now.setMonth(now.getMonth() - 1)))
const params: LoadRandomTopShoutsParams = { const params: LoadRandomTopShoutsParams = {
filters: { filters: getLoadShoutsFilters({ fromDate }),
visibility: 'public',
fromDate,
},
limit: 10, limit: 10,
fromRandomCount: 10, fromRandomCount: 10,
} }
params.filters = props.layout ? { layout: props.layout } : { excludeLayout: 'article' }
const result = await apiClient.getRandomTopShouts(params) const result = await apiClient.getRandomTopShouts(params)
setRandomTopMonthArticles(result) setRandomTopMonthArticles(result)
@ -103,9 +112,7 @@ export const Expo = (props: Props) => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) { if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore(LOAD_MORE_PAGE_SIZE) loadMore(LOAD_MORE_PAGE_SIZE)
} }
})
onMount(() => {
loadRandomTopArticles() loadRandomTopArticles()
loadRandomTopMonthArticles() loadRandomTopMonthArticles()
}) })

View File

@ -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 { getPagePath } from '@nanostores/router'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js' import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../../context/reactions'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../../stores/router'
import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles' import { useArticlesStore, resetSortedArticles } from '../../../stores/zine/articles'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../../stores/zine/topAuthors'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../../stores/zine/topics'
import { capitalize } from '../../utils/capitalize' import { apiClient } from '../../../utils/apiClient'
import { getImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { getDescription } from '../../utils/meta' import { Icon } from '../../_shared/Icon'
import { Icon } from '../_shared/Icon' import { Loading } from '../../_shared/Loading'
import { Loading } from '../_shared/Loading' import { CommentDate } from '../../Article/CommentDate'
import { CommentDate } from '../Article/CommentDate' import { AuthorLink } from '../../Author/AhtorLink'
import { AuthorLink } from '../Author/AhtorLink' import { AuthorBadge } from '../../Author/AuthorBadge'
import { AuthorBadge } from '../Author/AuthorBadge' import { ArticleCard } from '../../Feed/ArticleCard'
import { ArticleCard } from '../Feed/ArticleCard' import { Sidebar } from '../../Feed/Sidebar'
import { Sidebar } from '../Feed/Sidebar'
import styles from './Feed.module.scss' import styles from './Feed.module.scss'
import stylesBeside from '../../components/Feed/Beside.module.scss' import stylesBeside from '../../Feed/Beside.module.scss'
import stylesTopic from '../Feed/CardTopic.module.scss' import stylesTopic from '../../Feed/CardTopic.module.scss'
export const FEED_PAGE_SIZE = 20 export const FEED_PAGE_SIZE = 20
const UNRATED_ARTICLES_COUNT = 5
type FeedSearchParams = { type FeedSearchParams = {
by: 'publish_date' | 'rating' | 'last_comment' 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 { t } = useLocalize()
const { page, searchParams } = useRouter<FeedSearchParams>() const { page, searchParams } = useRouter<FeedSearchParams>()
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
@ -62,13 +62,20 @@ export const FeedView = (props: Props) => {
const { topAuthors } = useTopAuthorsStore() const { topAuthors } = useTopAuthorsStore()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [topComments, setTopComments] = createSignal<Reaction[]>([]) const [topComments, setTopComments] = createSignal<Reaction[]>([])
const [unratedArticles, setUnratedArticles] = createSignal<Shout[]>([])
const { const {
actions: { loadReactionsBy }, actions: { loadReactionsBy },
} = useReactions() } = useReactions()
const loadUnratedArticles = async () => {
const result = await apiClient.getUnratedShouts(UNRATED_ARTICLES_COUNT)
setUnratedArticles(result)
}
onMount(() => { onMount(() => {
loadMore() loadMore()
loadUnratedArticles()
}) })
createEffect( createEffect(
@ -271,6 +278,14 @@ export const FeedView = (props: Props) => {
</li> </li>
</ul> </ul>
</section> </section>
<Show when={unratedArticles().length > 0}>
<section class={clsx(styles.asideSection)}>
<h4>{t('Be the first to rate')}</h4>
<For each={unratedArticles()}>
{(article) => <ArticleCard article={article} settings={{ noimage: true, nodate: true }} />}
</For>
</section>
</Show>
</aside> </aside>
</div> </div>
</div> </div>

View File

@ -0,0 +1 @@
export { Feed } from './Feed'

View File

@ -29,7 +29,7 @@ export const SearchView = (props: Props) => {
const { searchParams } = useRouter<SearchPageSearchParams>() const { searchParams } = useRouter<SearchPageSearchParams>()
let searchEl: HTMLInputElement let searchEl: HTMLInputElement
const handleQueryChange = (_ev) => { const handleQueryChange = () => {
setQuery(searchEl.value) setQuery(searchEl.value)
} }

View File

@ -101,7 +101,7 @@
padding: 0; padding: 0;
& swiper-slide { & swiper-slide {
//bind to html element <swiper-slide/> // bind to html element <swiper-slide/>
width: unset !important; width: unset !important;
} }

View File

@ -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
}
}
}
`

View File

@ -377,6 +377,7 @@ export type Query = {
loadRecipients: Result loadRecipients: Result
loadShout?: Maybe<Shout> loadShout?: Maybe<Shout>
loadShouts: Array<Maybe<Shout>> loadShouts: Array<Maybe<Shout>>
loadUnratedShouts: Array<Maybe<Shout>>
markdownBody: Scalars['String']['output'] markdownBody: Scalars['String']['output']
myFeed?: Maybe<Array<Maybe<Shout>>> myFeed?: Maybe<Array<Maybe<Shout>>>
searchMessages: Result searchMessages: Result
@ -449,6 +450,10 @@ export type QueryLoadShoutsArgs = {
options?: InputMaybe<LoadShoutsOptions> options?: InputMaybe<LoadShoutsOptions>
} }
export type QueryLoadUnratedShoutsArgs = {
limit: Scalars['Int']['input']
}
export type QueryMarkdownBodyArgs = { export type QueryMarkdownBodyArgs = {
body: Scalars['String']['input'] body: Scalars['String']['input']
} }

View File

@ -5,7 +5,6 @@ import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show }
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Author' import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../components/Views/Author'
import { useLocalize } from '../context/localize'
import { ReactionsProvider } from '../context/reactions' import { ReactionsProvider } from '../context/reactions'
import { useRouter } from '../stores/router' import { useRouter } from '../stores/router'
import { loadShouts, resetSortedArticles } from '../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../stores/zine/articles'

View File

@ -1,6 +1,6 @@
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo } from 'solid-js' import { createEffect, createMemo, on } from 'solid-js'
import { PageLayout } from '../../components/_shared/PageLayout' import { PageLayout } from '../../components/_shared/PageLayout'
import { Topics } from '../../components/Nav/Topics' import { Topics } from '../../components/Nav/Topics'
@ -14,7 +14,7 @@ export const ExpoPage = (props: PageProps) => {
const { page } = useRouter() const { page } = useRouter()
const getLayout = createMemo<LayoutType>(() => page().params['layout'] as LayoutType) const getLayout = createMemo<LayoutType>(() => page().params['layout'] as LayoutType)
const title = createMemo(() => { const getTitle = () => {
switch (getLayout()) { switch (getLayout()) {
case 'music': { case 'music': {
return t('Audio') return t('Audio')
@ -32,10 +32,20 @@ export const ExpoPage = (props: PageProps) => {
return t('Art') return t('Art')
} }
} }
}) }
createEffect(
on(
() => getLayout(),
() => {
document.title = getTitle()
},
{ defer: true },
),
)
return ( return (
<PageLayout withPadding={true} zeroBottomPadding={true} title={title()}> <PageLayout withPadding={true} zeroBottomPadding={true} title={getTitle()}>
<Topics /> <Topics />
<Expo shouts={props.expoShouts} layout={getLayout()} /> <Expo shouts={props.expoShouts} layout={getLayout()} />
</PageLayout> </PageLayout>

View File

@ -2,7 +2,7 @@ import { createEffect, Match, on, onCleanup, Switch } from 'solid-js'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { AuthGuard } from '../components/AuthGuard' import { AuthGuard } from '../components/AuthGuard'
import { FeedView } from '../components/Views/Feed' import { Feed } from '../components/Views/Feed'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
import { ReactionsProvider } from '../context/reactions' import { ReactionsProvider } from '../context/reactions'
import { LoadShoutsOptions } from '../graphql/types.gen' import { LoadShoutsOptions } from '../graphql/types.gen'
@ -40,13 +40,13 @@ export const FeedPage = () => {
return ( return (
<PageLayout title={t('Feed')}> <PageLayout title={t('Feed')}>
<ReactionsProvider> <ReactionsProvider>
<Switch fallback={<FeedView loadShouts={handleFeedLoadShouts} />}> <Switch fallback={<Feed loadShouts={handleFeedLoadShouts} />}>
<Match when={page().route === 'feed'}> <Match when={page().route === 'feed'}>
<FeedView loadShouts={handleFeedLoadShouts} /> <Feed loadShouts={handleFeedLoadShouts} />
</Match> </Match>
<Match when={page().route === 'feedMy'}> <Match when={page().route === 'feedMy'}>
<AuthGuard> <AuthGuard>
<FeedView loadShouts={handleMyFeedLoadShouts} /> <Feed loadShouts={handleMyFeedLoadShouts} />
</AuthGuard> </AuthGuard>
</Match> </Match>
</Switch> </Switch>

View File

@ -4,16 +4,13 @@ import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from '
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../components/Views/Topic'
import { useLocalize } from '../context/localize'
import { ReactionsProvider } from '../context/reactions' import { ReactionsProvider } from '../context/reactions'
import { useRouter } from '../stores/router' import { useRouter } from '../stores/router'
import { loadShouts, resetSortedArticles } from '../stores/zine/articles' import { loadShouts, resetSortedArticles } from '../stores/zine/articles'
import { loadTopic } from '../stores/zine/topics' import { loadTopic } from '../stores/zine/topics'
import { capitalize } from '../utils/capitalize'
export const TopicPage = (props: PageProps) => { export const TopicPage = (props: PageProps) => {
const { page } = useRouter() const { page } = useRouter()
const { t } = useLocalize()
const slug = createMemo(() => page().params['slug'] as string) const slug = createMemo(() => page().params['slug'] as string)
const [isLoaded, setIsLoaded] = createSignal( const [isLoaded, setIsLoaded] = createSignal(

View File

@ -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 { createLazyMemo } from '@solid-primitives/memo'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
@ -179,14 +179,6 @@ export const resetSortedArticles = () => {
setSortedArticles([]) setSortedArticles([])
} }
export const createArticle = async ({ article }: { article: ShoutInput }) => {
try {
await apiClient.createArticle({ article })
} catch (error) {
console.error(error)
}
}
type InitialState = { type InitialState = {
shouts?: Shout[] shouts?: Shout[]
} }

View File

@ -44,8 +44,8 @@ import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import shoutLoad from '../graphql/query/article-load' import shoutLoad from '../graphql/query/article-load'
import shoutsLoadBy from '../graphql/query/articles-load-by' 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 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 authCheckEmailQuery from '../graphql/query/auth-check-email'
import authLoginQuery from '../graphql/query/auth-login' import authLoginQuery from '../graphql/query/auth-login'
import authorBySlug from '../graphql/query/author-by-slug' import authorBySlug from '../graphql/query/author-by-slug'
@ -362,6 +362,15 @@ export const apiClient = {
return resp.data.loadRandomTopShouts return resp.data.loadRandomTopShouts
}, },
getUnratedShouts: async (limit: number): Promise<Shout[]> => {
const resp = await publicGraphQLClient.query(articlesLoadUnrated, { limit }).toPromise()
if (resp.error) {
console.error(resp)
}
return resp.data.loadUnratedShouts
},
getMyFeed: async (options: LoadShoutsOptions) => { getMyFeed: async (options: LoadShoutsOptions) => {
const resp = await privateGraphQLClient.query(myFeed, { options }).toPromise() const resp = await privateGraphQLClient.query(myFeed, { options }).toPromise()