home-tops
This commit is contained in:
parent
ae207a02f2
commit
305bd23f9f
|
@ -4,6 +4,7 @@ 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)
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
ssr: true,
|
ssr: true,
|
||||||
server: {
|
server: {
|
||||||
|
|
|
@ -6,11 +6,13 @@
|
||||||
},
|
},
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"defaultBranch": "dev",
|
"defaultBranch": "dev",
|
||||||
"useIgnoreFile": true
|
"useIgnoreFile": true,
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git"
|
||||||
},
|
},
|
||||||
"organizeImports": {
|
"organizeImports": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"ignore": ["./api", "./gen"]
|
"ignore": ["./gen"]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
|
|
|
@ -62,8 +62,8 @@ const getTitleAndSubtitle = (
|
||||||
title: string
|
title: string
|
||||||
subtitle: string
|
subtitle: string
|
||||||
} => {
|
} => {
|
||||||
let title = article.title
|
let title = article?.title || ''
|
||||||
let subtitle: string = article.subtitle || ''
|
let subtitle: string = article?.subtitle || ''
|
||||||
|
|
||||||
if (!subtitle) {
|
if (!subtitle) {
|
||||||
let titleParts = article.title?.split('. ') || []
|
let titleParts = article.title?.split('. ') || []
|
||||||
|
@ -220,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<A href={`/article${props.article.slug}`}>
|
<A href={`/article${props.article?.slug || ''}`}>
|
||||||
<div class={styles.shoutCardTitle}>
|
<div class={styles.shoutCardTitle}>
|
||||||
<span class={styles.shoutCardLinkWrapper}>
|
<span class={styles.shoutCardLinkWrapper}>
|
||||||
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
<span class={styles.shoutCardLinkContainer} innerHTML={title} />
|
||||||
|
@ -229,7 +229,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
<Show when={!props.settings?.nosubtitle && subtitle}>
|
<Show when={!props.settings?.nosubtitle && subtitle}>
|
||||||
<div class={styles.shoutCardSubtitle}>
|
<div class={styles.shoutCardSubtitle}>
|
||||||
<span class={styles.shoutCardLinkContainer} innerHTML={subtitle} />
|
<span class={styles.shoutCardLinkContainer} innerHTML={subtitle || ''} />
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</A>
|
</A>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
|
||||||
import { useAuthors } from '~/context/authors'
|
import { useAuthors } from '~/context/authors'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useTopics } from '../../context/topics'
|
import { useTopics } from '../../context/topics'
|
||||||
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
import { Author, Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
import { capitalize } from '../../utils/capitalize'
|
import { capitalize } from '../../utils/capitalize'
|
||||||
import { splitToPages } from '../../utils/splitToPages'
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
import Banner from '../Discours/Banner'
|
import Banner from '../Discours/Banner'
|
||||||
|
@ -38,7 +38,7 @@ export interface HomeViewProps {
|
||||||
|
|
||||||
export const HomeView = (props: HomeViewProps) => {
|
export const HomeView = (props: HomeViewProps) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { topAuthors } = useAuthors()
|
const { topAuthors, addAuthors } = useAuthors()
|
||||||
const { topTopics, randomTopic } = useTopics()
|
const { topTopics, randomTopic } = useTopics()
|
||||||
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
||||||
createEffect(
|
createEffect(
|
||||||
|
@ -53,6 +53,9 @@ export const HomeView = (props: HomeViewProps) => {
|
||||||
})
|
})
|
||||||
const shouts = await shoutsByTopicLoader()
|
const shouts = await shoutsByTopicLoader()
|
||||||
setRandomTopicArticles(shouts || [])
|
setRandomTopicArticles(shouts || [])
|
||||||
|
shouts?.forEach((s: Shout) => addAuthors((s?.authors || []) as Author[]))
|
||||||
|
props.featuredShouts?.forEach((s: Shout) => addAuthors((s?.authors || []) as Author[]))
|
||||||
|
props.topRatedShouts?.forEach((s: Shout) => addAuthors((s?.authors || []) as Author[]))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ defer: true }
|
{ defer: true }
|
||||||
|
@ -84,7 +87,7 @@ export const HomeView = (props: HomeViewProps) => {
|
||||||
<Beside
|
<Beside
|
||||||
beside={props.featuredShouts[9]}
|
beside={props.featuredShouts[9]}
|
||||||
title={t('Top authors')}
|
title={t('Top authors')}
|
||||||
values={topAuthors()}
|
values={topAuthors?.() || []}
|
||||||
wrapper={'author'}
|
wrapper={'author'}
|
||||||
nodate={true}
|
nodate={true}
|
||||||
/>
|
/>
|
||||||
|
@ -96,7 +99,7 @@ export const HomeView = (props: HomeViewProps) => {
|
||||||
<Row1 article={props.featuredShouts[16]} nodate={true} />
|
<Row1 article={props.featuredShouts[16]} nodate={true} />
|
||||||
<Row3 articles={props.featuredShouts.slice(17, 20)} nodate={true} />
|
<Row3 articles={props.featuredShouts.slice(17, 20)} nodate={true} />
|
||||||
<Row3
|
<Row3
|
||||||
articles={props.topCommentedShouts.slice(0, 3)}
|
articles={props.topCommentedShouts?.slice(0, 3) || []}
|
||||||
header={<h2>{t('Top commented')}</h2>}
|
header={<h2>{t('Top commented')}</h2>}
|
||||||
nodate={true}
|
nodate={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -23,7 +23,8 @@ export const ArticleCardSwiper = (props: Props) => {
|
||||||
let mainSwipeRef: SwiperRef | null
|
let mainSwipeRef: SwiperRef | null
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (props.slides.length > 1) {
|
if (props.slides?.length > 1) {
|
||||||
|
console.debug(props.slides)
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation])
|
SwiperCore.use([Pagination, Navigation, Manipulation])
|
||||||
|
@ -34,16 +35,16 @@ export const ArticleCardSwiper = (props: Props) => {
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<div
|
<div
|
||||||
class={clsx({
|
class={clsx({
|
||||||
[styles.Swiper]: props.slides.length > 1,
|
[styles.Swiper]: props.slides?.length > 1,
|
||||||
[styles.articleMode]: true,
|
[styles.articleMode]: true,
|
||||||
[styles.ArticleCardSwiper]: props.slides.length > 1
|
[styles.ArticleCardSwiper]: props.slides?.length > 1
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Show when={props.title}>
|
<Show when={props.title}>
|
||||||
<h2 class={styles.sliderTitle}>{props.title}</h2>
|
<h2 class={styles.sliderTitle}>{props.title}</h2>
|
||||||
</Show>
|
</Show>
|
||||||
<div class={styles.container}>
|
<div class={styles.container}>
|
||||||
<Show when={props.slides.length > 0}>
|
<Show when={props.slides?.length > 0}>
|
||||||
<Show when={props.slides.length !== 1} fallback={<Row1 article={props.slides[0]} />}>
|
<Show when={props.slides.length !== 1} fallback={<Row1 article={props.slides[0]} />}>
|
||||||
<Show when={props.slides.length !== 2} fallback={<Row2 articles={props.slides} />}>
|
<Show when={props.slides.length !== 2} fallback={<Row2 articles={props.slides} />}>
|
||||||
<div class={styles.holder}>
|
<div class={styles.holder}>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router'
|
import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router'
|
||||||
import { Show, Suspense, createEffect, createSignal, on, onMount } from 'solid-js'
|
import { Show, Suspense, createEffect, createSignal, on, onMount } from 'solid-js'
|
||||||
|
import { LoadShoutsOptions } from '~/graphql/schema/core.gen'
|
||||||
import { loadShouts } from '~/lib/api'
|
import { loadShouts } from '~/lib/api'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
||||||
|
import { byStat } from '~/utils/sortby'
|
||||||
import { HomeView, HomeViewProps } from '../components/Views/Home'
|
import { HomeView, HomeViewProps } from '../components/Views/Home'
|
||||||
import { Loading } from '../components/_shared/Loading'
|
import { Loading } from '../components/_shared/Loading'
|
||||||
import { PageLayout } from '../components/_shared/PageLayout'
|
import { PageLayout } from '../components/_shared/PageLayout'
|
||||||
|
@ -11,32 +13,33 @@ import { ReactionsProvider } from '../context/reactions'
|
||||||
export const SHOUTS_PER_PAGE = 20
|
export const SHOUTS_PER_PAGE = 20
|
||||||
|
|
||||||
const fetchHomeTopData = async () => {
|
const fetchHomeTopData = async () => {
|
||||||
const limit = 20
|
|
||||||
|
|
||||||
const topCommentedLoader = loadShouts({
|
const topCommentedLoader = loadShouts({
|
||||||
filters: { featured: true },
|
filters: { featured: true },
|
||||||
limit
|
order_by: 'comments_stat',
|
||||||
|
limit: 10
|
||||||
})
|
})
|
||||||
|
|
||||||
const topMonthLoader = loadShouts({
|
const daysago = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||||
filters: { featured: true },
|
const after = Math.floor(daysago / 1000)
|
||||||
limit
|
const options: LoadShoutsOptions = {
|
||||||
})
|
filters: {
|
||||||
|
featured: true,
|
||||||
const topViewedLoader = loadShouts({
|
after
|
||||||
filters: { featured: true },
|
},
|
||||||
limit
|
order_by: 'likes_stat',
|
||||||
})
|
limit: 10
|
||||||
|
}
|
||||||
|
const topMonthLoader = loadShouts({ ...options })
|
||||||
|
|
||||||
const topRatedLoader = loadShouts({
|
const topRatedLoader = loadShouts({
|
||||||
filters: { featured: true },
|
filters: { featured: true },
|
||||||
limit
|
order_by: 'likes_stat',
|
||||||
|
limit: 10
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
topCommentedShouts: await topCommentedLoader(),
|
|
||||||
topMonthShouts: await topMonthLoader(),
|
|
||||||
topRatedShouts: await topRatedLoader(),
|
topRatedShouts: await topRatedLoader(),
|
||||||
topViewedShouts: await topViewedLoader()
|
topMonthShouts: await topMonthLoader(),
|
||||||
|
topCommentedShouts: await topCommentedLoader()
|
||||||
} as Partial<HomeViewProps>
|
} as Partial<HomeViewProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,12 +72,19 @@ export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
|
||||||
// async ssr-friendly router-level cached data source
|
// async ssr-friendly router-level cached data source
|
||||||
const data = createAsync(async (prev?: HomeViewProps) => {
|
const data = createAsync(async (prev?: HomeViewProps) => {
|
||||||
const featuredShoutsLoader = featuredLoader(featuredOffset())
|
const featuredShoutsLoader = featuredLoader(featuredOffset())
|
||||||
const featuredShouts = await featuredShoutsLoader()
|
const featuredShouts = [
|
||||||
return {
|
...(prev?.featuredShouts || []),
|
||||||
|
...((await featuredShoutsLoader()) || props.data?.featuredShouts || [])
|
||||||
|
]
|
||||||
|
const sortFn = byStat('viewed')
|
||||||
|
const topViewedShouts = featuredShouts?.sort(sortFn) || []
|
||||||
|
const result = {
|
||||||
...prev,
|
...prev,
|
||||||
...props.data,
|
...props.data,
|
||||||
featuredShouts: featuredShouts || prev?.featuredShouts || props.data?.featuredShouts
|
topViewedShouts,
|
||||||
|
featuredShouts
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
const [canLoadMoreFeatured, setCanLoadMoreFeatured] = createSignal(false)
|
const [canLoadMoreFeatured, setCanLoadMoreFeatured] = createSignal(false)
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const ArticlePage = (props: RouteSectionProps<{ article: Shout }>) => {
|
||||||
() => `${article()?.authors?.[0]?.name || t('Discours')}: ${article()?.title || ''}`
|
() => `${article()?.authors?.[0]?.name || t('Discours')}: ${article()?.title || ''}`
|
||||||
)
|
)
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if(gaIdentity) {
|
if (gaIdentity) {
|
||||||
try {
|
try {
|
||||||
console.info('[routes.slug] mounted, connecting ga...')
|
console.info('[routes.slug] mounted, connecting ga...')
|
||||||
await loadGAScript(gaIdentity)
|
await loadGAScript(gaIdentity)
|
||||||
|
@ -44,10 +44,11 @@ export const ArticlePage = (props: RouteSectionProps<{ article: Shout }>) => {
|
||||||
// wrapped by the returned tracking function is notified of a change`
|
// wrapped by the returned tracking function is notified of a change`
|
||||||
createReaction(() => {
|
createReaction(() => {
|
||||||
if (article()) {
|
if (article()) {
|
||||||
|
console.debug('[routes.slug] article signal changed once')
|
||||||
window.gtag?.('event', 'page_view', {
|
window.gtag?.('event', 'page_view', {
|
||||||
page_title: article()?.title,
|
page_title: article()?.title,
|
||||||
page_location: window.location.href,
|
page_location: window.location.href,
|
||||||
page_path: window.location.pathname,
|
page_path: window.location.pathname
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,10 +19,12 @@ export const loadGAScript = (id: string) => {
|
||||||
export const initGA = (id: string) => {
|
export const initGA = (id: string) => {
|
||||||
const w = window as Window
|
const w = window as Window
|
||||||
if (w) {
|
if (w) {
|
||||||
|
// @ts-ignore
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: ga-script
|
// biome-ignore lint/suspicious/noExplicitAny: ga-script
|
||||||
w.dataLayer = (w.dataLayer as any) || []
|
w.dataLayer = (w.dataLayer as any) || []
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: ga-script
|
// biome-ignore lint/suspicious/noExplicitAny: ga-script
|
||||||
function gtag(...args: any[]) {
|
function gtag(...args: any[]) {
|
||||||
|
// @ts-ignore
|
||||||
w.dataLayer.push(args)
|
w.dataLayer.push(args)
|
||||||
}
|
}
|
||||||
gtag('js', new Date())
|
gtag('js', new Date())
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Author, Reaction, Shout, Topic, TopicStat } from '../graphql/schema/core.gen'
|
import type { Author, Maybe, Reaction, Shout, Topic, TopicStat } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: sort by first char
|
// biome-ignore lint/suspicious/noExplicitAny: sort by first char
|
||||||
export const byFirstChar = (a: { name?: any; title?: any }, b: { name?: any; title?: any }) =>
|
export const byFirstChar = (a: { name?: any; title?: any }, b: { name?: any; title?: any }) =>
|
||||||
|
@ -26,8 +26,10 @@ export const byLength = (
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SomeStat = { [x: string]: Maybe<number> } | undefined | null
|
||||||
|
|
||||||
export const byStat = (metric: string) => {
|
export const byStat = (metric: string) => {
|
||||||
return (a: { stat: { [x: string]: number } }, b: { stat: { [x: string]: number } }) => {
|
return (a: { stat?: SomeStat }, b: { stat?: SomeStat }) => {
|
||||||
const aStat = a.stat?.[metric] ?? 0
|
const aStat = a.stat?.[metric] ?? 0
|
||||||
const bStat = b.stat?.[metric] ?? 0
|
const bStat = b.stat?.[metric] ?? 0
|
||||||
return aStat - bStat
|
return aStat - bStat
|
||||||
|
|
Loading…
Reference in New Issue
Block a user