diff --git a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx index 6a011431..10a740a7 100644 --- a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx +++ b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx @@ -4,7 +4,7 @@ import { Icon } from '~/components/_shared/Icon' import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' import { MediaItem } from '~/types/mediaitem' -import { getArticleDescription } from '~/utils/meta' +import { descFromBody } from '~/utils/meta' import { SharePopup, getShareUrl } from '../SharePopup' import styles from './AudioPlayer.module.scss' @@ -137,7 +137,7 @@ export const PlayerPlaylist = (props: Props) => { > { const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false) const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true) - const description = getArticleDescription(props.article?.body) + const description = descFromBody(props.article?.body) const aspectRatio: Accessor = () => LAYOUT_ASPECT[props.article?.layout as string] const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang()) const { title, subtitle } = getTitleAndSubtitle(props.article) diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 59b0e425..251a3e99 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -4,7 +4,7 @@ import { useLocalize } from '~/context/localize' import { useTopics } from '~/context/topics' import { loadShouts } from '~/graphql/api/public' import { Author, Shout, Topic } from '~/graphql/schema/core.gen' -import { SHOUTS_PER_PAGE } from '~/routes/(home)' +import { SHOUTS_PER_PAGE } from '~/routes/(main)' import { capitalize } from '~/utils/capitalize' import { splitToPages } from '~/utils/splitToPages' import Banner from '../Discours/Banner' diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index c5c43901..b9116c6f 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -7,7 +7,7 @@ import { useLocalize } from '~/context/localize' import { useTopics } from '~/context/topics' import { loadAuthors, loadFollowersByTopic, loadShouts } from '~/graphql/api/public' import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' -import { SHOUTS_PER_PAGE } from '~/routes/(home)' +import { SHOUTS_PER_PAGE } from '~/routes/(main)' import { getUnixtime } from '~/utils/getServerDate' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { splitToPages } from '~/utils/splitToPages' diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index 3b431e54..e8322e30 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -8,7 +8,7 @@ import { Shout } from '~/graphql/schema/core.gen' import enKeywords from '~/intl/locales/en/keywords.json' import ruKeywords from '~/intl/locales/ru/keywords.json' import { getImageUrl, getOpenGraphImageUrl } from '~/lib/getImageUrl' -import { getArticleKeywords } from '~/utils/meta' +import { descFromBody } from '~/utils/meta' import { FooterView } from '../Discours/Footer' import { Header } from '../Nav/Header' import styles from './PageLayout.module.scss' @@ -50,7 +50,7 @@ export const PageLayout = (props: PageLayoutProps) => { const keypath = createMemo(() => (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords) const keywords = createMemo( () => - (props.article && getArticleKeywords(props.article as Shout)) || + (props.article && descFromBody(props.article.body)) || (lang() === 'ru' ? ruKeywords[keypath()] : enKeywords[keypath()]) ) const [scrollToComments, setScrollToComments] = createSignal(false) diff --git a/src/routes/(home).tsx b/src/routes/(main).tsx similarity index 100% rename from src/routes/(home).tsx rename to src/routes/(main).tsx diff --git a/src/routes/[slug].tsx b/src/routes/[slug].tsx deleted file mode 100644 index b0e9746c..00000000 --- a/src/routes/[slug].tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { RouteDefinition, RouteSectionProps, createAsync, useLocation, useParams } from '@solidjs/router' -import { HttpStatusCode } from '@solidjs/start' -import { - ErrorBoundary, - Show, - Suspense, - createEffect, - createMemo, - createSignal, - on, - onMount -} from 'solid-js' -import { FourOuFourView } from '~/components/Views/FourOuFour' -import { Loading } from '~/components/_shared/Loading' -import { gaIdentity } from '~/config' -import { useLocalize } from '~/context/localize' -import { getShout } from '~/graphql/api/public' -import type { Reaction, Shout } from '~/graphql/schema/core.gen' -import { initGA, loadGAScript } from '~/utils/ga' -import { getArticleKeywords } from '~/utils/meta' -import { FullArticle } from '../components/Article/FullArticle' -import { PageLayout } from '../components/_shared/PageLayout' -import { ReactionsProvider } from '../context/reactions' - -const fetchShout = async (slug: string): Promise => { - const shoutLoader = getShout({ slug }) - const result = await shoutLoader() - return result -} - -export const route: RouteDefinition = { - load: async ({ params }) => ({ - article: await fetchShout(params.slug) - }) -} - -export default ( - props: RouteSectionProps<{ article?: Shout; comments?: Reaction[]; votes?: Reaction[] }> -) => { - const params = useParams() - const loc = useLocation() - const { t } = useLocalize() - const [scrollToComments, setScrollToComments] = createSignal(false) - const article = createAsync(async () => props.data.article || (await fetchShout(params.slug))) - - const title = createMemo( - () => `${article()?.authors?.[0]?.name || t('Discours')} :: ${article()?.title || ''}` - ) - - onMount(async () => { - if (gaIdentity && article()?.id) { - try { - await loadGAScript(gaIdentity) - initGA(gaIdentity) - } catch (error) { - console.warn('Failed to connect Google Analytics:', error) - } - } - }) - - createEffect( - on( - article, - (a?: Shout) => { - if (!a?.id) return - window?.gtag?.('event', 'page_view', { - page_title: a.title, - page_location: window?.location.href || '', - page_path: loc.pathname - }) - }, - { defer: true } - ) - ) - - return ( - }> - }> - - - - - } - > - setScrollToComments(value)} - > - - - - - - - - ) -} diff --git a/src/routes/[slug]/(author-or-post).tsx b/src/routes/[slug]/(author-or-post).tsx new file mode 100644 index 00000000..b3c17e10 --- /dev/null +++ b/src/routes/[slug]/(author-or-post).tsx @@ -0,0 +1,134 @@ +import { RouteDefinition, RouteSectionProps, createAsync, useLocation, useParams } from '@solidjs/router' +import { HttpStatusCode } from '@solidjs/start' +import { + ErrorBoundary, + Show, + Suspense, + createEffect, + createMemo, + createSignal, + on, + onMount +} from 'solid-js' +import { FourOuFourView } from '~/components/Views/FourOuFour' +import { Loading } from '~/components/_shared/Loading' +import { gaIdentity } from '~/config' +import { useLocalize } from '~/context/localize' +import { getAuthor, getShout } from '~/graphql/api/public' +import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' +import { initGA, loadGAScript } from '~/utils/ga' +import { descFromBody, keywordsFromTopics } from '~/utils/meta' +import { FullArticle } from '../../components/Article/FullArticle' +import { PageLayout } from '../../components/_shared/PageLayout' +import { ReactionsProvider } from '../../context/reactions' +import AuthorPage, { AuthorPageProps } from '../author/[slug]/[...tab]' + +const fetchShout = async (slug: string): Promise => { + const shoutLoader = getShout({ slug }) + const result = await shoutLoader() + return result +} + +const fetchAuthor = async (slug: string): Promise => { + const authorLoader = getAuthor({ slug }) + const result = await authorLoader() + return result +} + +export const route: RouteDefinition = { + load: async ({ params }) => ({ + article: await fetchShout(params.slug) + }) +} + +type SlugPageProps = { article?: Shout; comments?: Reaction[]; votes?: Reaction[]; author?: Author } + +export default (props: RouteSectionProps) => { + const params = useParams() + if (params.slug.startsWith('@')) return AuthorPage(props as RouteSectionProps) + + const loc = useLocation() + const { t } = useLocalize() + const [scrollToComments, setScrollToComments] = createSignal(false) + const article = createAsync(async () => props.data.article || (await fetchShout(params.slug))) + const author = createAsync(async () => + params.slug.startsWith('@') + ? props.data.author || (await fetchAuthor(params.slug)) + : article()?.authors?.[0] + ) + const titleSuffix = createMemo( + () => (article()?.title || author()?.name) ?? ` :: ${article()?.title || author()?.name || ''}` + ) + + onMount(async () => { + if (gaIdentity && article()?.id) { + try { + await loadGAScript(gaIdentity) + initGA(gaIdentity) + } catch (error) { + console.warn('Failed to connect Google Analytics:', error) + } + } + }) + + createEffect( + on( + article, + (a?: Shout) => { + if (!a?.id) return + window?.gtag?.('event', 'page_view', { + page_title: a.title, + page_location: window?.location.href || '', + page_path: loc.pathname + }) + }, + { defer: true } + ) + ) + + return ( + }> + }> + + + + + } + > + setScrollToComments(value)} + > + + + + + } + > + + + + + + + + + + ) +} diff --git a/src/routes/author/[slug]/[...tab].tsx b/src/routes/author/[slug]/[...tab].tsx index 647daa0d..d065afc1 100644 --- a/src/routes/author/[slug]/[...tab].tsx +++ b/src/routes/author/[slug]/[...tab].tsx @@ -16,7 +16,7 @@ import { Topic } from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getImageUrl' -import { SHOUTS_PER_PAGE } from '../../(home)' +import { SHOUTS_PER_PAGE } from '../../(main)' const fetchAuthorShouts = async (slug: string, offset?: number) => { const opts: LoadShoutsOptions = { filters: { author: slug }, limit: SHOUTS_PER_PAGE, offset } @@ -47,7 +47,9 @@ export const route = { } } -export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; topics: Topic[] }>) => { +export type AuthorPageProps = { articles?: Shout[]; author?: Author; topics?: Topic[] } + +export const AuthorPage = (props: RouteSectionProps) => { const params = useParams() const { addAuthor } = useAuthors() const articles = createAsync( @@ -55,7 +57,7 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to ) const author = createAsync(async () => { const a = props.data.author || (await fetchAuthor(params.slug)) - addAuthor(a) + a && addAuthor(a) return a }) const topics = createAsync(async () => props.data.topics || (await fetchAllTopics())) @@ -104,3 +106,5 @@ export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; to ) } + +export default AuthorPage diff --git a/src/routes/expo/[layout].tsx b/src/routes/expo/[layout].tsx index 2bf092d9..427c7a2b 100644 --- a/src/routes/expo/[layout].tsx +++ b/src/routes/expo/[layout].tsx @@ -7,7 +7,7 @@ import { useLocalize } from '~/context/localize' import { loadShouts } from '~/graphql/api/public' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' -import { SHOUTS_PER_PAGE } from '../(home)' +import { SHOUTS_PER_PAGE } from '../(main)' const fetchExpoShouts = async (layouts: string[]) => { const result = await loadShouts({ diff --git a/src/routes/feed/(feed).tsx b/src/routes/feed/(feed).tsx index 157a5d92..f39fee14 100644 --- a/src/routes/feed/(feed).tsx +++ b/src/routes/feed/(feed).tsx @@ -7,7 +7,7 @@ import { useLocalize } from '~/context/localize' import { ReactionsProvider } from '~/context/reactions' import { loadShouts } from '~/graphql/api/public' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' -import { SHOUTS_PER_PAGE } from '../(home)' +import { SHOUTS_PER_PAGE } from '../(main)' export type FeedPeriod = 'week' | 'month' | 'year' diff --git a/src/routes/topic/[slug].tsx b/src/routes/topic/[slug].tsx index 9de1e41c..f30be97b 100644 --- a/src/routes/topic/[slug].tsx +++ b/src/routes/topic/[slug].tsx @@ -10,8 +10,8 @@ import { useTopics } from '~/context/topics' import { loadShouts, loadTopics } from '~/graphql/api/public' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getImageUrl' -import { getArticleDescription } from '~/utils/meta' -import { SHOUTS_PER_PAGE } from '../(home)' +import { descFromBody } from '~/utils/meta' +import { SHOUTS_PER_PAGE } from '../(main)' const fetchTopicShouts = async (slug: string, offset?: number) => { const opts: LoadShoutsOptions = { filters: { topic: slug }, limit: SHOUTS_PER_PAGE, offset } @@ -72,7 +72,7 @@ export default (props: RouteSectionProps<{ articles: Shout[]; topics: Topic[] }> const desc = createMemo(() => topic()?.body - ? getArticleDescription(topic()?.body || '') + ? descFromBody(topic()?.body || '') : t('The most interesting publications on the topic', { topicName: title() }) ) diff --git a/src/utils/meta.ts b/src/utils/meta.ts index 46a807fa..aff32bea 100644 --- a/src/utils/meta.ts +++ b/src/utils/meta.ts @@ -1,16 +1,14 @@ -import { Shout } from '~/graphql/schema/core.gen' - const MAX_DESCRIPTION_LENGTH = 150 -export const getArticleDescription = (body: string): string => { +export const descFromBody = (body: string): string => { if (!body) { return '' } const descriptionWordsArray = body - .replaceAll(/<[^>]*>/g, ' ') - .replaceAll(/\s+/g, ' ') + .replace(/<[^>]*>/g, ' ') // Remove HTML tags + .replace(/\s+/g, ' ') // Normalize whitespace .split(' ') - // ¯\_(ツ)_/¯ maybe need to remove the punctuation + let description = '' let i = 0 while (i < descriptionWordsArray.length && description.length < MAX_DESCRIPTION_LENGTH) { @@ -20,6 +18,6 @@ export const getArticleDescription = (body: string): string => { return description.trim() } -export const getArticleKeywords = (shout: Shout): string => { - return (shout.topics || [])?.map((topic) => topic?.title).join(', ') +export const keywordsFromTopics = (topics: { title: string }[]): string => { + return topics.map((topic: { title: string }) => topic.title).join(', ') }