mainpage-loadmore-hide

This commit is contained in:
Untone 2024-07-18 12:22:28 +03:00
parent ab05a9e539
commit 7573c6334c
10 changed files with 112 additions and 72 deletions

View File

@ -8,6 +8,8 @@ import sassDts from 'vite-plugin-sass-dts'
const isVercel = Boolean(process?.env.VERCEL) const isVercel = Boolean(process?.env.VERCEL)
const isBun = Boolean(process.env.BUN) const isBun = Boolean(process.env.BUN)
console.info(`[app.config] build for ${isVercel ? 'vercel' : isBun? 'bun' : 'node'}!`)
const polyfillOptions = { const polyfillOptions = {
include: ['path', 'stream', 'util'], include: ['path', 'stream', 'util'],
exclude: ['http'], exclude: ['http'],

View File

@ -1,9 +1,8 @@
import { A, useLocation } from '@solidjs/router' import { A, useLocation } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { useFeed } from '~/context/feed'
import { useFollowing } from '~/context/following' import { useFollowing } from '~/context/following'
import { useGraphQL } from '~/context/graphql' import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
@ -28,7 +27,6 @@ type AuthorViewProps = {
selectedTab: string selectedTab: string
shouts?: Shout[] shouts?: Shout[]
author?: Author author?: Author
topics?: Topic[]
} }
export const PRERENDERED_ARTICLES_COUNT = 12 export const PRERENDERED_ARTICLES_COUNT = 12
@ -40,7 +38,6 @@ export const AuthorView = (props: AuthorViewProps) => {
const loc = useLocation() const loc = useLocation()
const { session } = useSession() const { session } = useSession()
const { query } = useGraphQL() const { query } = useGraphQL()
const { sortedFeed } = useFeed()
const { loadAuthor, authorsEntities } = useAuthors() const { loadAuthor, authorsEntities } = useAuthors()
const { followers: myFollowers, follows: myFollows } = useFollowing() const { followers: myFollowers, follows: myFollows } = useFollowing()
@ -55,7 +52,7 @@ export const AuthorView = (props: AuthorViewProps) => {
// derivatives // derivatives
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author) const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const pages = createMemo<Shout[][]>(() => const pages = createMemo<Shout[][]>(() =>
paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) paginate((props.shouts || []).slice(1), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
) )
// 1 // проверяет не собственный ли это профиль, иначе - загружает // 1 // проверяет не собственный ли это профиль, иначе - загружает
@ -107,20 +104,20 @@ export const AuthorView = (props: AuthorViewProps) => {
// event handlers // event handlers
let bioContainerRef: HTMLDivElement let bioContainerRef: HTMLDivElement
let bioWrapperRef: HTMLDivElement let bioWrapperRef: HTMLDivElement
const checkBioHeight = () => { const checkBioHeight = (bio = bioWrapperRef) => {
if (bioContainerRef) { if (!bio) return
const showExpand = bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight const showExpand = bioContainerRef.offsetHeight > bio.offsetHeight
setShowExpandBioControl(showExpand) setShowExpandBioControl(showExpand)
console.debug('[AuthorView] mounted, show expand bio container:', showExpand) console.debug('[AuthorView] mounted, show expand bio container:', showExpand)
} }
}
const handleDeleteComment = (id: number) => { const handleDeleteComment = (id: number) => {
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id)) setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
} }
// on load // on load
onMount(checkBioHeight) createEffect(on(() => bioContainerRef, checkBioHeight))
createEffect(on(() => props.selectedTab, (tab) => tab && console.log('[views.Author] profile tab switched')))
return ( return (
<div class={styles.authorPage}> <div class={styles.authorPage}>
@ -227,18 +224,18 @@ export const AuthorView = (props: AuthorViewProps) => {
</div> </div>
</Show> </Show>
<Show when={sortedFeed().length > 0}> <Show when={Array.isArray(props.shouts) && props.shouts.length > 0 && props.shouts[0]}>
<Row1 article={sortedFeed()[0]} noauthor={true} nodate={true} /> <Row1 article={props.shouts?.[0] as Shout} noauthor={true} nodate={true} />
<Show when={sortedFeed().length > 1}> <Show when={props.shouts && props.shouts.length > 1}>
<Switch> <Switch>
<Match when={sortedFeed().length === 2}> <Match when={props.shouts && props.shouts.length === 2}>
<Row2 articles={sortedFeed()} isEqual={true} noauthor={true} nodate={true} /> <Row2 articles={props.shouts as Shout[]} isEqual={true} noauthor={true} nodate={true} />
</Match> </Match>
<Match when={sortedFeed().length === 3}> <Match when={props.shouts && props.shouts.length === 3}>
<Row3 articles={sortedFeed()} noauthor={true} nodate={true} /> <Row3 articles={props.shouts as Shout[]} noauthor={true} nodate={true} />
</Match> </Match>
<Match when={sortedFeed().length > 3}> <Match when={props.shouts && props.shouts.length > 3}>
<For each={pages()}> <For each={pages()}>
{(page) => ( {(page) => (
<> <>

View File

@ -39,6 +39,12 @@ export type FeedProps = {
order?: '' | 'likes' | 'hot' order?: '' | 'likes' | 'hot'
} }
const PERIODS = {
'day': 24 * 60 * 60,
'month': 30 * 24 * 60 * 60,
'year': 365 * 24 * 60 * 60
}
export const FeedView = (props: FeedProps) => { export const FeedView = (props: FeedProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const loc = useLocation() const loc = useLocation()
@ -59,9 +65,6 @@ export const FeedView = (props: FeedProps) => {
const { topAuthors } = useAuthors() const { topAuthors } = useAuthors()
const [topComments, setTopComments] = createSignal<Reaction[]>([]) const [topComments, setTopComments] = createSignal<Reaction[]>([])
const [searchParams, changeSearchParams] = useSearchParams<FeedSearchParams>() const [searchParams, changeSearchParams] = useSearchParams<FeedSearchParams>()
const asOption = (o: string) => ({ value: o, title: t(o) })
const asOptions = (opts: string[]) => opts.map(asOption)
const currentPeriod = createMemo(() => asOption(searchParams?.period || ''))
const loadTopComments = async () => { const loadTopComments = async () => {
const comments = await loadReactionsBy({ by: { comment: true }, limit: 50 }) const comments = await loadReactionsBy({ by: { comment: true }, limit: 50 })
setTopComments(comments.sort(byCreated).reverse()) setTopComments(comments.sort(byCreated).reverse())
@ -94,6 +97,13 @@ export const FeedView = (props: FeedProps) => {
setShareData(shared) setShareData(shared)
} }
const asOption = (o: string) => {
const value = Math.floor(Date.now()/1000) - PERIODS[o as keyof typeof PERIODS]
return { value, title: t(o) }
}
const asOptions = (opts: string[]) => opts.map(asOption)
const currentPeriod = createMemo(() => asOption(searchParams?.period || ''))
return ( return (
<div class="feed"> <div class="feed">
<div class="row"> <div class="row">

View File

@ -89,7 +89,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
if (data.entity === 'reaction' && authorized()) { if (data.entity === 'reaction' && authorized()) {
console.info('[context.notifications] event', data) console.info('[context.notifications] event', data)
loadNotificationsGrouped({ loadNotificationsGrouped({
after: after() || Date.now(), after: after() || now,
limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) limit: Math.max(PAGE_SIZE, loadedNotificationsCount())
}) })
} }
@ -108,14 +108,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => {
const markSeenAll = async () => { const markSeenAll = async () => {
if (authorized()) { if (authorized()) {
const _resp = await mutation(markSeenAfterMutation, { after: after() }).toPromise() const _resp = await mutation(markSeenAfterMutation, { after: after() }).toPromise()
await loadNotificationsGrouped({ after: after() || Date.now(), limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() })
} }
} }
const markSeen = async (notification_id: number) => { const markSeen = async (notification_id: number) => {
if (authorized()) { if (authorized()) {
await mutation(markSeenMutation, { notification_id }).toPromise() await mutation(markSeenMutation, { notification_id }).toPromise()
await loadNotificationsGrouped({ after: after() || Date.now(), limit: loadedNotificationsCount() }) await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() })
} }
} }

View File

@ -58,6 +58,10 @@ export const loadShouts = (options: LoadShoutsOptions) => {
} }
export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => { export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => {
if (!options.by) {
console.debug(options)
throw new Error('[api] wrong loadReactions call')
}
const kind = options.by?.comment ? 'comments' : options.by?.rating ? 'votes' : 'reactions' const kind = options.by?.comment ? 'comments' : options.by?.rating ? 'votes' : 'reactions'
const allorone = options.by?.shout ? `shout-${options.by.shout}` : 'all' const allorone = options.by?.shout ? `shout-${options.by.shout}` : 'all'
const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}` const page = `${options.offset || 0}-${(options?.limit || 0) + (options.offset || 0)}`

View File

@ -45,6 +45,7 @@
"Artworks": "Артворки", "Artworks": "Артворки",
"Audio": "Аудио", "Audio": "Аудио",
"Author": "Автор", "Author": "Автор",
"author profile was not found": "не удалось найти профиль автора",
"Authors": "Авторы", "Authors": "Авторы",
"Autotypograph": "Автотипограф", "Autotypograph": "Автотипограф",
"Back": "Назад", "Back": "Назад",

View File

@ -109,7 +109,7 @@ export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
return ( return (
<PageLayout withPadding={true} title={t('Discours')} key="home"> <PageLayout withPadding={true} title={t('Discours')} key="home">
<LoadMoreWrapper loadFunction={loadMoreFeatured} pageSize={SHOUTS_PER_PAGE}> <LoadMoreWrapper loadFunction={loadMoreFeatured} pageSize={SHOUTS_PER_PAGE} hidden={!featuredFeed()}>
<HomeView <HomeView
featuredShouts={featuredFeed() || (shouts() as Shout[])} featuredShouts={featuredFeed() || (shouts() as Shout[])}
topMonthShouts={topMonthFeed() as Shout[]} topMonthShouts={topMonthFeed() as Shout[]}

View File

@ -39,7 +39,7 @@ type SlugPageProps = {
export default (props: RouteSectionProps<SlugPageProps>) => { export default (props: RouteSectionProps<SlugPageProps>) => {
if (props.params.slug.startsWith('@')) { if (props.params.slug.startsWith('@')) {
console.debug('[slug] starts with @, render as author page') console.debug('[routes] [slug]/[...tab] starts with @, render as author page')
const patchedProps = { const patchedProps = {
...props, ...props,
params: { params: {
@ -51,7 +51,7 @@ export default (props: RouteSectionProps<SlugPageProps>) => {
} }
if (props.params.slug.startsWith('!')) { if (props.params.slug.startsWith('!')) {
console.debug('[slug] starts with !, render as topic page') console.debug('[routes] [slug]/[...tab] starts with !, render as topic page')
const patchedProps = { const patchedProps = {
...props, ...props,
params: { params: {
@ -74,7 +74,7 @@ export default (props: RouteSectionProps<SlugPageProps>) => {
await loadGAScript(gaIdentity) await loadGAScript(gaIdentity)
initGA(gaIdentity) initGA(gaIdentity)
} catch (error) { } catch (error) {
console.warn('Failed to connect Google Analytics:', error) console.warn('[routes] [slug]/[...tab] Failed to connect Google Analytics:', error)
} }
} }
}) })

View File

@ -1,5 +1,5 @@
import { RouteSectionProps, createAsync } from '@solidjs/router' import { RouteSectionProps } from '@solidjs/router'
import { ErrorBoundary, createEffect, createMemo } from 'solid-js' import { ErrorBoundary, createEffect, createMemo, createSignal, on } from 'solid-js'
import { AuthorView } from '~/components/Views/Author' import { AuthorView } from '~/components/Views/Author'
import { FourOuFourView } from '~/components/Views/FourOuFour' import { FourOuFourView } from '~/components/Views/FourOuFour'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
@ -13,6 +13,9 @@ import {
Author, Author,
LoadShoutsOptions, LoadShoutsOptions,
QueryLoad_Authors_ByArgs, QueryLoad_Authors_ByArgs,
QueryLoad_Reactions_ByArgs,
Reaction,
ReactionKind,
Shout, Shout,
Topic Topic
} from '~/graphql/schema/core.gen' } from '~/graphql/schema/core.gen'
@ -25,6 +28,16 @@ const fetchAuthorShouts = async (slug: string, offset?: number) => {
return await shoutsLoader() return await shoutsLoader()
} }
const fetchAuthorComments = async (slug: string, offset?: number) => {
const opts: QueryLoad_Reactions_ByArgs = {
by: { comment: true, author: slug },
limit: SHOUTS_PER_PAGE,
offset
}
const shoutsLoader = loadReactions(opts)
return await shoutsLoader()
}
const fetchAllTopics = async () => { const fetchAllTopics = async () => {
const topicsFetcher = loadTopics() const topicsFetcher = loadTopics()
return await topicsFetcher() return await topicsFetcher()
@ -51,18 +64,8 @@ export type AuthorPageProps = { articles?: Shout[]; author?: Author; topics?: To
export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) { export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
const { addAuthor, authorsEntities } = useAuthors() const { addAuthor, authorsEntities } = useAuthors()
const articles = createAsync( const [author, setAuthor] = createSignal<Author | undefined>(undefined)
async () => props.data.articles || (await fetchAuthorShouts(props.params.slug)) || []
)
const author = createAsync(async () => {
const loadedBefore = authorsEntities()[props.params.slug]
if (loadedBefore) return loadedBefore
const a = props.data.author || (await fetchAuthor(props.params.slug))
a && addAuthor(a)
return a
})
const topics = createAsync(async () => props.data.topics || (await fetchAllTopics()))
const { t } = useLocalize() const { t } = useLocalize()
const title = createMemo(() => `${author()?.name || ''}`) const title = createMemo(() => `${author()?.name || ''}`)
@ -83,28 +86,44 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
: getImageUrl('production/image/logo_image.png') : getImageUrl('production/image/logo_image.png')
) )
const selectedTab = createMemo(() => (props.params.tab in ['comments', 'about'] ? props.params.tab : '')) // author comments
const { addReactions } = useReactions() const { addReactions, reactionEntities } = useReactions()
const loadMoreComments = async () => { const commentsByAuthor = createMemo(() =>
const commentsFetcher = loadReactions({ Object.values(reactionEntities).filter(
by: { comment: true, created_by: author()?.id } (r: Reaction) => r.kind === ReactionKind.Comment && r.created_by.id === author()?.id
}) )
const ccc = await commentsFetcher() )
ccc && addReactions(ccc) // author shouts
return ccc as LoadMoreItems
}
const { addFeed, feedByAuthor } = useFeed() const { addFeed, feedByAuthor } = useFeed()
const loadMoreAuthorShouts = async () => { const shoutsByAuthor = createMemo(() => feedByAuthor()[props.params.slug])
const slug = author()?.slug
const offset = feedByAuthor()[props.params.slug].length createEffect(
const shoutsFetcher = loadShouts({ on(
filters: { author: slug }, [() => props.params.slug || '', author],
offset, async ([slug, profile]) => {
limit: SHOUTS_PER_PAGE if (!profile) {
}) const loadedAuthor = authorsEntities()[slug] || (await fetchAuthor(slug))
const sss = await shoutsFetcher() if (loadedAuthor) {
sss && addFeed(sss) addAuthor(loadedAuthor)
return sss as LoadMoreItems setAuthor(loadedAuthor)
}
}
},
{ defer: true }
)
)
const loadAuthorDataMore = async (offset = 0) => {
if (props.params.tab === 'comments') {
const commentsOffset = commentsByAuthor().length
const loadedComments = await fetchAuthorComments(props.params.slug, commentsOffset)
loadedComments && addReactions(loadedComments)
return (loadedComments || []) as LoadMoreItems
}
const shoutsOffset = shoutsByAuthor().length
const loadedShouts = await fetchAuthorShouts(props.params.slug, shoutsOffset)
loadedShouts && addFeed(loadedShouts)
return (loadedShouts || []) as LoadMoreItems
} }
return ( return (
@ -118,16 +137,15 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
> >
<ReactionsProvider> <ReactionsProvider>
<LoadMoreWrapper <LoadMoreWrapper
loadFunction={selectedTab() === 'comments' ? loadMoreComments : loadMoreAuthorShouts} loadFunction={loadAuthorDataMore}
pageSize={SHOUTS_PER_PAGE} pageSize={SHOUTS_PER_PAGE}
hidden={selectedTab() !== '' || selectedTab() !== 'comments'} hidden={!props.params.tab || props.params.tab !== 'comments'}
> >
<AuthorView <AuthorView
author={author() as Author} author={author() as Author}
selectedTab={selectedTab()} selectedTab={props.params.tab}
authorSlug={props.params.slug} authorSlug={props.params.slug}
shouts={feedByAuthor()[props.params.slug] || (articles() as Shout[])} shouts={shoutsByAuthor()}
topics={topics()}
/> />
</LoadMoreWrapper> </LoadMoreWrapper>
</ReactionsProvider> </ReactionsProvider>

View File

@ -7,6 +7,7 @@ import { Icon } from '~/components/_shared/Icon'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { useGraphQL } from '~/context/graphql' import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSnackbar } from '~/context/ui'
import createShoutMutation from '~/graphql/mutation/core/article-create' import createShoutMutation from '~/graphql/mutation/core/article-create'
import styles from '~/styles/Create.module.scss' import styles from '~/styles/Create.module.scss'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
@ -14,11 +15,18 @@ import { LayoutType } from '~/types/common'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
const client = useGraphQL() const client = useGraphQL()
const {showSnackbar} = useSnackbar()
const navigate = useNavigate() const navigate = useNavigate()
const handleCreate = async (layout: LayoutType) => { const handleCreate = async (layout: LayoutType) => {
console.debug('[routes : edit/new] handling create click...')
const result = await client.mutation(createShoutMutation, { shout: { layout: layout } }).toPromise() const result = await client.mutation(createShoutMutation, { shout: { layout: layout } }).toPromise()
if (result) { if (result) {
const shout = result.data.create_shout console.debug(result)
const {shout, error} = result.data.create_shout
if (error) showSnackbar({
body: `${t('Error')}: ${t(error)}`,
type: 'error'
})
if (shout?.id) navigate(`/edit/${shout.id}`) if (shout?.id) navigate(`/edit/${shout.id}`)
} }
} }
@ -34,8 +42,8 @@ export default () => {
<ul class={clsx('nodash', styles.list)}> <ul class={clsx('nodash', styles.list)}>
<For each={['Article', 'Literature', 'Image', 'Audio', 'Video']}> <For each={['Article', 'Literature', 'Image', 'Audio', 'Video']}>
{(layout: string) => ( {(layout: string) => (
<li> <li onClick={() => handleCreate(layout.toLowerCase() as LayoutType)}>
<div class={styles.link} onClick={() => handleCreate(layout.toLowerCase() as LayoutType)}> <div class={styles.link}>
<Icon name={`create-${layout.toLowerCase()}`} class={styles.icon} /> <Icon name={`create-${layout.toLowerCase()}`} class={styles.icon} />
<div>{t(layout)}</div> <div>{t(layout)}</div>
</div> </div>