From e3ac3cc40628a93e6dbd8ac46ccdb419f85b3f3d Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 9 Jul 2024 12:13:13 +0300 Subject: [PATCH] meta-refactored --- src/components/Article/FullArticle.tsx | 37 +-- src/components/AuthorsList/AuthorsList.tsx | 99 ------- src/components/AuthorsList/index.ts | 1 - src/components/Nav/Header/Header.tsx | 9 +- src/components/Nav/Topics/index.ts | 1 - .../TopicsNav.module.scss} | 0 .../Topics.tsx => TopicsNav/TopicsNav.tsx} | 4 +- src/components/Nav/TopicsNav/index.ts | 1 + src/components/Topic/Full.tsx | 17 +- .../Views/AllAuthors/AllAuthors.tsx | 275 ++++++++++-------- .../AllAuthors}/AuthorsList.module.scss | 0 src/components/Views/AllTopics/AllTopics.tsx | 27 +- src/components/Views/Author/Author.tsx | 34 +-- src/components/Views/Feed/Feed.tsx | 24 +- src/components/Views/Home.tsx | 11 +- src/components/Views/StaticPage.tsx | 42 +-- src/components/Views/Topic.tsx | 48 +-- src/components/_shared/PageLayout.tsx | 73 +++-- src/context/reactions.tsx | 2 +- src/graphql/api/public.ts | 2 +- src/intl/locales/en/keywords.json | 2 +- src/intl/locales/ru/keywords.json | 2 +- src/routes/(home).tsx | 2 +- src/routes/[slug].tsx | 3 +- src/routes/author/(all-authors).tsx | 26 +- src/routes/author/[slug]/[tab].tsx | 11 +- src/routes/edit/new.tsx | 28 +- src/routes/expo/[layout].tsx | 4 +- src/routes/feed/[feed].tsx | 7 +- src/routes/topic/(all-topics).tsx | 7 +- src/routes/topic/[slug].tsx | 17 +- 31 files changed, 329 insertions(+), 487 deletions(-) delete mode 100644 src/components/AuthorsList/AuthorsList.tsx delete mode 100644 src/components/AuthorsList/index.ts delete mode 100644 src/components/Nav/Topics/index.ts rename src/components/Nav/{Topics/Topics.module.scss => TopicsNav/TopicsNav.module.scss} (100%) rename src/components/Nav/{Topics/Topics.tsx => TopicsNav/TopicsNav.tsx} (95%) create mode 100644 src/components/Nav/TopicsNav/index.ts rename src/components/{AuthorsList => Views/AllAuthors}/AuthorsList.module.scss (100%) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 17891d64..25863788 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,6 +1,6 @@ // import { install } from 'ga-gtag' import { createPopper } from '@popperjs/core' -import { Link, Meta } from '@solidjs/meta' +import { Link } from '@solidjs/meta' import { A, useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' @@ -12,10 +12,9 @@ import { useSession } from '~/context/session' import { DEFAULT_HEADER_OFFSET, useUI } from '~/context/ui' import type { Author, Maybe, QueryLoad_Reactions_ByArgs, Shout, Topic } from '~/graphql/schema/core.gen' import { isCyrillic } from '~/intl/translate' -import { getImageUrl, getOpenGraphImageUrl } from '~/lib/getImageUrl' +import { getImageUrl } from '~/lib/getImageUrl' import { MediaItem } from '~/types/mediaitem' import { capitalize } from '~/utils/capitalize' -import { getArticleDescription, getArticleKeywords } from '~/utils/meta' import { AuthorBadge } from '../Author/AuthorBadge' import { CardTopic } from '../Feed/CardTopic' import { FeedArticlePopup } from '../Feed/FeedArticlePopup' @@ -302,7 +301,6 @@ export const FullArticle = (props: Props) => { ) onMount(async () => { - // install('G-LQ4B87H8C2') const opts: QueryLoad_Reactions_ByArgs = { by: { shout: props.article.slug }, limit: 999, offset: 0 } const _rrr = await loadReactionsBy(opts) addSeen(props.article.slug) @@ -326,34 +324,11 @@ export const FullArticle = (props: Props) => { }) }) - const cover = props.article.cover || 'production/image/logo_image.png' - const ogImage = getOpenGraphImageUrl(cover, { - title: props.article.title, - topic: mainTopic()?.title || '', - author: props.article.authors?.[0]?.name || '', - width: 1200 - }) - - const description = getArticleDescription(props.article.description || body() || media()[0]?.body) - const ogTitle = props.article.title - const keywords = getArticleKeywords(props.article) const shareUrl = getShareUrl({ pathname: `/${props.article.slug || ''}` }) - const getAuthorName = (a: Author) => { - return lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name - } + const getAuthorName = (a: Author) => + lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name return ( <> - - - - - - - - - - - {(imageUrl) => }
@@ -522,7 +497,7 @@ export const FullArticle = (props: Props) => {
{ diff --git a/src/components/AuthorsList/AuthorsList.tsx b/src/components/AuthorsList/AuthorsList.tsx deleted file mode 100644 index 95305042..00000000 --- a/src/components/AuthorsList/AuthorsList.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { clsx } from 'clsx' -import { For, Show, createEffect, createSignal, on } from 'solid-js' -import { useAuthors } from '~/context/authors' -import { useLocalize } from '~/context/localize' -import { loadAuthors } from '~/graphql/api/public' -import { Author } from '~/graphql/schema/core.gen' -import { AuthorBadge } from '../Author/AuthorBadge' -import { InlineLoader } from '../InlineLoader' -import { AUTHORS_PER_PAGE } from '../Views/AllAuthors/AllAuthors' -import { Button } from '../_shared/Button' -import styles from './AuthorsList.module.scss' - -type Props = { - class?: string - query: 'followers' | 'shouts' - searchQuery?: string - allAuthorsLength?: number -} - -// pagination handling, loadAuthors cached from api, addAuthors to context - -export const AuthorsList = (props: Props) => { - const { t } = useLocalize() - const { addAuthors } = useAuthors() - const [authorsByShouts, setAuthorsByShouts] = createSignal() - const [authorsByFollowers, setAuthorsByFollowers] = createSignal() - const [loading, setLoading] = createSignal(false) - const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 }) - const [allLoaded, setAllLoaded] = createSignal(false) - - const fetchAuthors = async (queryType: Props['query'], page: number) => { - setLoading(true) - const offset = AUTHORS_PER_PAGE * page - const fetcher = await loadAuthors({ - by: { order: queryType }, - limit: AUTHORS_PER_PAGE, - offset - }) - const result = await fetcher() - if (result) { - addAuthors([...result]) - if (queryType === 'shouts') { - setAuthorsByShouts((prev) => [...(prev || []), ...result]) - } else if (queryType === 'followers') { - setAuthorsByFollowers((prev) => [...(prev || []), ...result]) - } - setLoading(false) - } - } - - const loadMoreAuthors = () => { - const nextPage = currentPage()[props.query] + 1 - fetchAuthors(props.query, nextPage).then(() => - setCurrentPage({ ...currentPage(), [props.query]: nextPage }) - ) - } - - createEffect( - on( - () => props.query, - (query) => { - const al = query === 'shouts' ? authorsByShouts() : authorsByFollowers() - if (al?.length === 0 && currentPage()[query] === 0) { - setCurrentPage((prev) => ({ ...prev, [query]: 0 })) - fetchAuthors(query, 0).then(() => setCurrentPage((prev) => ({ ...prev, [query]: 1 }))) - } - } - ) - ) - - const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers()) - createEffect(() => setAllLoaded(props.allAuthorsLength === authorsList.length)) - - return ( -
- - {(author) => ( -
-
- -
-
- )} -
-
-
-
- 0 && !allLoaded()}> -
-
-
-
- ) -} diff --git a/src/components/AuthorsList/index.ts b/src/components/AuthorsList/index.ts deleted file mode 100644 index 4187ebae..00000000 --- a/src/components/AuthorsList/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AuthorsList } from './AuthorsList' diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index 776dabbd..aeda0fd1 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -7,7 +7,6 @@ import { useTopics } from '~/context/topics' import { useUI } from '~/context/ui' import type { Topic } from '../../../graphql/schema/core.gen' import { getRandomTopicsFromArray } from '../../../lib/getRandomTopicsFromArray' -import { getArticleDescription } from '../../../utils/meta' import { SharePopup, getShareUrl } from '../../Article/SharePopup' import { Icon } from '../../_shared/Icon' import { Newsletter } from '../../_shared/Newsletter' @@ -24,7 +23,7 @@ type Props = { title?: string slug?: string isHeaderFixed?: boolean - articleBody?: string + desc?: string cover?: string scrollToComments?: (value: boolean) => void } @@ -324,10 +323,8 @@ export const Header = (props: Props) => { title={props.title || ''} imageUrl={props.cover || ''} shareUrl={getShareUrl()} - description={getArticleDescription(props.articleBody?.slice(0, 100) || '')} - onVisibilityChange={(isVisible) => { - setIsSharePopupVisible(isVisible) - }} + description={props.desc || ''} + onVisibilityChange={setIsSharePopupVisible} containerCssClass={styles.control} trigger={ <> diff --git a/src/components/Nav/Topics/index.ts b/src/components/Nav/Topics/index.ts deleted file mode 100644 index c6ce8d87..00000000 --- a/src/components/Nav/Topics/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Topics } from './Topics' diff --git a/src/components/Nav/Topics/Topics.module.scss b/src/components/Nav/TopicsNav/TopicsNav.module.scss similarity index 100% rename from src/components/Nav/Topics/Topics.module.scss rename to src/components/Nav/TopicsNav/TopicsNav.module.scss diff --git a/src/components/Nav/Topics/Topics.tsx b/src/components/Nav/TopicsNav/TopicsNav.tsx similarity index 95% rename from src/components/Nav/Topics/Topics.tsx rename to src/components/Nav/TopicsNav/TopicsNav.tsx index a5eac959..874e37c5 100644 --- a/src/components/Nav/Topics/Topics.tsx +++ b/src/components/Nav/TopicsNav/TopicsNav.tsx @@ -3,9 +3,9 @@ import { Icon } from '~/components/_shared/Icon' import { useLocalize } from '~/context/localize' import { A, useMatch } from '@solidjs/router' -import styles from './Topics.module.scss' +import styles from './TopicsNav.module.scss' -export const Topics = () => { +export const TopicsNav = () => { const { t } = useLocalize() const matchExpo = useMatch(() => '/expo') return ( diff --git a/src/components/Nav/TopicsNav/index.ts b/src/components/Nav/TopicsNav/index.ts new file mode 100644 index 00000000..37653d78 --- /dev/null +++ b/src/components/Nav/TopicsNav/index.ts @@ -0,0 +1 @@ +export { TopicsNav } from './TopicsNav' diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 15c35d84..09baba15 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -1,7 +1,7 @@ import type { Author, Topic } from '~/graphql/schema/core.gen' import { clsx } from 'clsx' -import { Show, createEffect, createSignal } from 'solid-js' +import { Show, createEffect, createMemo, createSignal } from 'solid-js' import { useFollowing } from '~/context/following' import { useLocalize } from '~/context/localize' @@ -9,6 +9,7 @@ import { useSession } from '~/context/session' import { FollowingEntity } from '~/graphql/schema/core.gen' import { Button } from '../_shared/Button' +import { capitalize } from '~/utils/capitalize' import { FollowingCounters } from '../_shared/FollowingCounters/FollowingCounters' import { Icon } from '../_shared/Icon' import styles from './Full.module.scss' @@ -20,11 +21,21 @@ type Props = { } export const FullTopic = (props: Props) => { - const { t } = useLocalize() + const { t, lang } = useLocalize() const { follows, changeFollowing } = useFollowing() const { requireAuthentication } = useSession() const [followed, setFollowed] = createSignal() + const title = createMemo( + () => + // FIXME: use title translation + `#${capitalize( + lang() === 'en' + ? props.topic.slug.replace(/-/, ' ') + : props.topic.title || props.topic.slug.replace(/-/, ' '), + true + )}` + ) createEffect(() => { if (follows?.topics?.length !== 0) { const items = follows.topics || [] @@ -42,7 +53,7 @@ export const FullTopic = (props: Props) => { return (
-

#{props.topic?.title}

+

{title()}

diff --git a/src/components/Views/AllAuthors/AllAuthors.tsx b/src/components/Views/AllAuthors/AllAuthors.tsx index 3cadd431..996811a4 100644 --- a/src/components/Views/AllAuthors/AllAuthors.tsx +++ b/src/components/Views/AllAuthors/AllAuthors.tsx @@ -1,25 +1,24 @@ -import { Meta } from '@solidjs/meta' import { useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import { AuthorBadge } from '~/components/Author/AuthorBadge' +import { InlineLoader } from '~/components/InlineLoader' +import { Button } from '~/components/_shared/Button' import { Loading } from '~/components/_shared/Loading' import { SearchField } from '~/components/_shared/SearchField' import { useAuthors } from '~/context/authors' import { useLocalize } from '~/context/localize' import type { Author } from '~/graphql/schema/core.gen' -import enKeywords from '~/intl/locales/en/keywords.json' -import ruKeywords from '~/intl/locales/ru/keywords.json' import { authorLetterReduce, translateAuthor } from '~/intl/translate' import { dummyFilter } from '~/lib/dummyFilter' -import { getImageUrl } from '~/lib/getImageUrl' import { scrollHandler } from '~/utils/scroll' -import { AuthorsList } from '../../AuthorsList' import styles from './AllAuthors.module.scss' +import stylesAuthorList from './AuthorsList.module.scss' type Props = { authors: Author[] - topFollowedAuthors?: Author[] - topWritingAuthors?: Author[] + authorsByFollowers?: Author[] + authorsByShouts?: Author[] isLoaded: boolean } export const AUTHORS_PER_PAGE = 20 @@ -34,8 +33,9 @@ export const AllAuthors = (props: Props) => { const { t, lang } = useLocalize() const alphabet = createMemo(() => ABC[lang()] || ABC['ru']) const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>() - const { authorsSorted, setAuthorsSort } = useAuthors() + const { authorsSorted, setAuthorsSort, loadAuthors } = useAuthors() const authors = createMemo(() => props.authors || authorsSorted()) + const [loading, setLoading] = createSignal(false) // filter const [searchQuery, setSearchQuery] = createSignal('') @@ -52,7 +52,8 @@ export const AllAuthors = (props: Props) => { // store by first char const byLetterFiltered = createMemo<{ [letter: string]: Author[] }>(() => { - console.debug('[components.AllAuthors] byLetterFiltered') + if (!(filteredAuthors()?.length > 0)) return {} + console.debug('[components.AllAuthors] update byLetterFiltered', filteredAuthors()?.length) return ( filteredAuthors()?.reduce( (acc, author: Author) => authorLetterReduce(acc, author, lang()), @@ -69,120 +70,164 @@ export const AllAuthors = (props: Props) => { return keys }) - const ogImage = createMemo(() => getImageUrl('production/image/logo_image.png')) - const ogTitle = createMemo(() => t('Authors')) - const description = createMemo(() => t('List of authors of the open editorial community')) - - return ( -
- - - - - - - - - - - }> -
-
-
-

{t('Authors')}

-

{t('Subscribe who you like to tune your personal feed')}

- -
-
+ const fetchAuthors = async (queryType: string, page: number) => { + try { + console.debug('[components.AuthorsList] fetching authors...') + setLoading(true) + setAuthorsSort?.(queryType) + const offset = AUTHORS_PER_PAGE * page + await loadAuthors({ + by: { order: queryType }, + limit: AUTHORS_PER_PAGE, + offset + }) + } catch (error) { + console.error('[components.AuthorsList] error fetching authors:', error) + } finally { + setLoading(false) + } + } + const [currentPage, setCurrentPage] = createSignal<{ followers: number; shouts: number }>({ + followers: 0, + shouts: 0 + }) + const loadMoreAuthors = () => { + const by = searchParams?.by as 'followers' | 'shouts' | undefined + if (!by) return + const nextPage = currentPage()[by] + 1 + fetchAuthors(by, nextPage).then(() => setCurrentPage({ ...currentPage(), [by]: nextPage })) + } + const TabNavigator = () => ( +
+
+

{t('Authors')}

+

{t('Subscribe who you like to tune your personal feed')}

+ +
+
+ ) + + const AbcNavigator = () => ( + + ) + + const AbcAuthorsList = () => ( + + {(letter) => ( +
+

{letter}

+
- - {(letter) => ( -
-

{letter}

-
-
-
-
- - {(author) => ( -
-
- {translateAuthor(author, lang())} - - {author.stat?.shouts || 0} - -
-
- )} -
+
+
+ + {(author) => ( +
+
+ {translateAuthor(author, lang())} + + {author.stat?.shouts || 0} +
-
-
+ )} +
- )} - - - - +
+
+
+
+ )} +
+ ) + + const AuthorsSortedList = () => ( +
+ + {(author) => ( +
+
+ +
+
+ )} +
+
+
+
+ 0}> +
+
+
+
+ ) + return ( + <> + }> +
+ + + }> + +
-
+ ) } diff --git a/src/components/AuthorsList/AuthorsList.module.scss b/src/components/Views/AllAuthors/AuthorsList.module.scss similarity index 100% rename from src/components/AuthorsList/AuthorsList.module.scss rename to src/components/Views/AllAuthors/AuthorsList.module.scss diff --git a/src/components/Views/AllTopics/AllTopics.tsx b/src/components/Views/AllTopics/AllTopics.tsx index 00c01d9a..08664a59 100644 --- a/src/components/Views/AllTopics/AllTopics.tsx +++ b/src/components/Views/AllTopics/AllTopics.tsx @@ -1,4 +1,3 @@ -import { Meta } from '@solidjs/meta' import { A, useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' @@ -7,10 +6,7 @@ import { SearchField } from '~/components/_shared/SearchField' import { useLocalize } from '~/context/localize' import { useTopics } from '~/context/topics' import type { Topic } from '~/graphql/schema/core.gen' -import enKeywords from '~/intl/locales/en/keywords.json' -import ruKeywords from '~/intl/locales/ru/keywords.json' import { dummyFilter } from '~/lib/dummyFilter' -import { getImageUrl } from '~/lib/getImageUrl' import { capitalize } from '~/utils/capitalize' import { scrollHandler } from '~/utils/scroll' import { TopicBadge } from '../../Topic/TopicBadge' @@ -98,26 +94,9 @@ export const AllTopics = (props: Props) => {
) - - // meta - const ogImage = getImageUrl('production/image/logo_image.png') - const ogTitle = t('Themes and plots') - const description = t( - 'Thematic table of contents of the magazine. Here you can find all the topics that the community authors wrote about' - ) - return ( -
- - - - - - - - - - + <> +

{t('Themes and plots')}

}>
@@ -199,6 +178,6 @@ export const AllTopics = (props: Props) => {
-
+ ) } diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index dffc7613..3f824a44 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -1,4 +1,3 @@ -import { Meta, Title } from '@solidjs/meta' import { A, useLocation, useParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' @@ -10,14 +9,12 @@ import { useGraphQL } from '~/context/graphql' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useUI } from '~/context/ui' +import { loadReactions } from '~/graphql/api/public' import loadShoutsQuery from '~/graphql/query/core/articles-load-by' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' -import loadReactionsBy from '~/graphql/query/core/reactions-load-by' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' -import { getImageUrl } from '~/lib/getImageUrl' import { byCreated } from '~/lib/sortby' -import { getArticleDescription } from '~/utils/meta' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { splitToPages } from '~/utils/splitToPages' import stylesArticle from '../../Article/Article.module.scss' @@ -57,7 +54,7 @@ export const AuthorView = (props: Props) => { const [followers, setFollowers] = createSignal([] as Author[]) const [following, changeFollowing] = createSignal>([] as Array) // flat AuthorFollowsResult const [showExpandBioControl, setShowExpandBioControl] = createSignal(false) - const [commented, setCommented] = createSignal() + const [commented, setCommented] = createSignal([]) const { query } = useGraphQL() // пагинация загрузки ленты постов @@ -123,11 +120,11 @@ export const AuthorView = (props: Props) => { if (!commented() && profile) { await loadMore() - const resp = await query(loadReactionsBy, { + const commentsFetcher = loadReactions({ by: { comment: true, created_by: profile.id } - }).toPromise() - const ccc = resp?.data?.load_reactions_by - if (ccc) setCommented(ccc) + }) + const ccc = await commentsFetcher() + if (ccc) setCommented((_) => ccc || []) } } // { defer: true }, @@ -150,31 +147,12 @@ export const AuthorView = (props: Props) => { const pages = createMemo(() => splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) - - const ogImage = createMemo(() => - author()?.pic - ? getImageUrl(author()?.pic || '', { width: 1200 }) - : getImageUrl('production/image/logo_image.png') - ) - const description = createMemo(() => getArticleDescription(author()?.bio || '')) const handleDeleteComment = (id: number) => { setCommented((prev) => (prev || []).filter((comment) => comment.id !== id)) } return (
- - {author()?.name} - - - - - - - - - -
}> <> diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 3fcd7a9b..dadf0596 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -1,4 +1,3 @@ -import { Meta } from '@solidjs/meta' import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createMemo, createSignal, onMount } from 'solid-js' @@ -17,9 +16,6 @@ import { useTopics } from '~/context/topics' import { useUI } from '~/context/ui' import { loadUnratedShouts } from '~/graphql/api/private' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' -import ruKeywords from '~/intl/locales/ru/keywords.json' -import enKeywords from '~/intl/locales/ru/keywords.json' -import { getImageUrl } from '~/lib/getImageUrl' import { byCreated } from '~/lib/sortby' import { FeedSearchParams } from '~/routes/feed/[feed]' import { CommentDate } from '../../Article/CommentDate' @@ -41,7 +37,7 @@ export type FeedProps = { } export const FeedView = (props: FeedProps) => { - const { t, lang } = useLocalize() + const { t } = useLocalize() const loc = useLocation() const client = useGraphQL() const unrated = createAsync(async () => { @@ -79,12 +75,6 @@ export const FeedView = (props: FeedProps) => { }) }) - const ogImage = getImageUrl('production/image/logo_image.png') - const description = createMemo(() => - t('Independent media project about culture, science, art and society with horizontal editing') - ) - const ogTitle = createMemo(() => t('Feed')) - const [shareData, setShareData] = createSignal() const handleShare = (shared: Shout | undefined) => { showModal('share') @@ -92,17 +82,7 @@ export const FeedView = (props: FeedProps) => { } return ( -
- - - - - - - - - - +
diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index 3d2f8480..59b0e425 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -1,13 +1,9 @@ import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js' - -import { Meta } from '@solidjs/meta' import { useAuthors } from '~/context/authors' 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 enKeywords from '~/intl/locales/en/keywords.json' -import ruKeywords from '~/intl/locales/ru/keywords.json' import { SHOUTS_PER_PAGE } from '~/routes/(home)' import { capitalize } from '~/utils/capitalize' import { splitToPages } from '~/utils/splitToPages' @@ -20,7 +16,7 @@ import { Row2 } from '../Feed/Row2' import { Row3 } from '../Feed/Row3' import { Row5 } from '../Feed/Row5' import RowShort from '../Feed/RowShort' -import { Topics } from '../Nav/Topics' +import { TopicsNav } from '../Nav/TopicsNav' import { Icon } from '../_shared/Icon' import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper' import styles from './Home.module.scss' @@ -40,7 +36,7 @@ export interface HomeViewProps { } export const HomeView = (props: HomeViewProps) => { - const { t, lang } = useLocalize() + const { t } = useLocalize() const { topAuthors, addAuthors } = useAuthors() const { topTopics, randomTopic } = useTopics() const [randomTopicArticles, setRandomTopicArticles] = createSignal([]) @@ -75,9 +71,8 @@ export const HomeView = (props: HomeViewProps) => { return ( <> - 0}> - + SHOUTS_PER_PAGE}> diff --git a/src/components/Views/StaticPage.tsx b/src/components/Views/StaticPage.tsx index b50f09c8..b0411e73 100644 --- a/src/components/Views/StaticPage.tsx +++ b/src/components/Views/StaticPage.tsx @@ -1,10 +1,5 @@ -import { Meta } from '@solidjs/meta' -import { JSX, createMemo, onMount } from 'solid-js' -import { useLocalize } from '~/context/localize' -import enKeywords from '~/intl/locales/en/keywords.json' -import ruKeywords from '~/intl/locales/ru/keywords.json' +import { JSX, onMount } from 'solid-js' import { processPrepositions } from '~/intl/prepositions' -import { getImageUrl } from '~/lib/getImageUrl' import { TableOfContents } from '../TableOfContents' import { PageLayout } from '../_shared/PageLayout' @@ -15,39 +10,16 @@ type Props = { } export const StaticPage = (props: Props) => { - let articleBodyElement: HTMLElement | null = null - const { t, lang } = useLocalize() - const ogTitle = createMemo(() => t(props.title || 'Discours')) - const description = createMemo(() => t(props.desc || '')) - const ogImage = getImageUrl('production/image/logo_image.png') - const keywords = createMemo(() => { - const page = props.title.toLocaleLowerCase() as keyof typeof ruKeywords - return `${lang() === 'ru' ? ruKeywords[page] : enKeywords[page]}` - }) - let bodyEl: HTMLDivElement | undefined + let bodyEl: HTMLElement | undefined onMount(() => { if (bodyEl) bodyEl.innerHTML = processPrepositions(bodyEl.innerHTML) }) return ( - - - - - - - - - - - -
(articleBodyElement = el)} - > + +
(bodyEl = el)}>
-
(bodyEl = el)}> -

{ogTitle()}

+
+

{props.title}

{props.children}
@@ -55,7 +27,7 @@ export const StaticPage = (props: Props) => {
diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 8945c5ed..3bc4bba2 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -1,10 +1,6 @@ -import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' - -import { Meta } from '@solidjs/meta' +import { useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' - -import { useSearchParams } from '@solidjs/router' import { useAuthors } from '~/context/authors' import { useFeed } from '~/context/feed' import { useGraphQL } from '~/context/graphql' @@ -14,12 +10,8 @@ import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-t import loadShoutsRandomQuery from '~/graphql/query/core/articles-load-random-topic' import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by' import getTopicFollowersQuery from '~/graphql/query/core/topic-followers' -import enKeywords from '~/intl/locales/en/keywords.json' -import ruKeywords from '~/intl/locales/ru/keywords.json' -import { getImageUrl } from '~/lib/getImageUrl' -import { capitalize } from '~/utils/capitalize' +import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { getUnixtime } from '~/utils/getServerDate' -import { getArticleDescription } from '~/utils/meta' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { splitToPages } from '~/utils/splitToPages' import styles from '../../styles/Topic.module.scss' @@ -45,7 +37,7 @@ export const PRERENDERED_ARTICLES_COUNT = 28 const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: Props) => { - const { t, lang } = useLocalize() + const { t } = useLocalize() const { query } = useGraphQL() const [searchParams, changeSearchParams] = useSearchParams() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) @@ -55,7 +47,6 @@ export const TopicView = (props: Props) => { const { authorsByTopic } = useAuthors() const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) - const [topic, setTopic] = createSignal() createEffect( on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => { @@ -113,16 +104,6 @@ export const TopicView = (props: Props) => { } } - const title = createMemo( - () => - `#${capitalize( - lang() === 'en' - ? (topic() as Topic)?.slug.replace(/-/, ' ') - : (topic() as Topic)?.title || (topic() as Topic)?.slug.replace(/-/, ' '), - true - )}` - ) - const loadMore = async () => { saveScrollPosition() @@ -154,31 +135,8 @@ export const TopicView = (props: Props) => { const pages = createMemo(() => splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) - - const ogImage = () => - topic()?.pic - ? getImageUrl(topic()?.pic || '', { width: 1200 }) - : getImageUrl('production/image/logo_image.png') - const description = () => - topic()?.body - ? getArticleDescription(topic()?.body || '') - : t('The most interesting publications on the topic', { topicName: title() }) - return (
- - - - - - - - - -
diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index 7e804f70..de24c9bb 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -1,20 +1,24 @@ -import type { JSX } from 'solid-js' - -import { Title } from '@solidjs/meta' +import { Meta, Title } from '@solidjs/meta' +import { useLocation } from '@solidjs/router' import { clsx } from 'clsx' -import { Show, createEffect, createSignal } from 'solid-js' - +import type { JSX } from 'solid-js' +import { Show, createEffect, createMemo, createSignal } from 'solid-js' +import { useLocalize } from '~/context/localize' +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 { FooterView } from '../Discours/Footer' import { Header } from '../Nav/Header' - -import '../../styles/app.scss' import styles from './PageLayout.module.scss' -type Props = { +type PageLayoutProps = { title: string + desc?: string headerTitle?: string slug?: string - articleBody?: string + article?: Shout cover?: string children: JSX.Element isHeaderFixed?: boolean @@ -23,29 +27,56 @@ type Props = { withPadding?: boolean zeroBottomPadding?: boolean scrollToComments?: (value: boolean) => void + key?: string } -export const PageLayout = (props: Props) => { - const isHeaderFixed = props.isHeaderFixed === undefined ? true : props.isHeaderFixed +export const PageLayout = (props: PageLayoutProps) => { + const isHeaderFixed = props.isHeaderFixed === undefined ? true : props.isHeaderFixed // FIXME: выглядит как костылек + const loc = useLocation() + const { t, lang } = useLocalize() + const imageUrl = props.cover ? getImageUrl(props.cover) : 'production/image/logo_image.png' + const ogImage = createMemo(() => + // NOTE: preview generation logic works only for one article view + props.article + ? getOpenGraphImageUrl(imageUrl, { + title: props.title, + topic: props.article?.topics?.[0]?.title || '', + author: props.article?.authors?.[0]?.name || '', + width: 1200 + }) + : imageUrl + ) + const ogTitle = createMemo(() => t(props.title)) + const description = createMemo(() => (props.desc ? t(props.desc) : '')) + const keypath = createMemo(() => (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords) + const keywords = createMemo( + () => + (props.article && getArticleKeywords(props.article as Shout)) || + (lang() === 'ru' ? ruKeywords[keypath()] : enKeywords[keypath()]) + ) const [scrollToComments, setScrollToComments] = createSignal(false) - - createEffect(() => { - if (props.scrollToComments) { - props.scrollToComments(scrollToComments()) - } - }) - + createEffect(() => props.scrollToComments?.(scrollToComments())) return ( <> {props.title}
setScrollToComments(value)} /> + + + + + + + + + +
{ })} classList={{ 'main-content--no-padding': !isHeaderFixed }} > - {props.children} +
{props.children}
diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 320c7b48..f90b9769 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -40,7 +40,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const { mutation } = useGraphQL() const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise => { - const fetcher = await loadReactions({ ...opts }) + const fetcher = await loadReactions(opts) const result = (await fetcher()) || [] const newReactionsByShout: Record = {} const newReactionEntities = result.reduce( diff --git a/src/graphql/api/public.ts b/src/graphql/api/public.ts index cb7dc5c0..8c9c98c4 100644 --- a/src/graphql/api/public.ts +++ b/src/graphql/api/public.ts @@ -60,7 +60,7 @@ export const loadReactions = (options: QueryLoad_Reactions_ByArgs) => { const filter = new URLSearchParams(options.by as Record) console.debug(options) return cache(async () => { - const resp = await defaultClient.query(loadReactionsByQuery, { ...options }).toPromise() + const resp = await defaultClient.query(loadReactionsByQuery, options).toPromise() const result = resp?.data?.load_reactions_by if (result) return result as Reaction[] }, `reactions-${filter}-${page}`) diff --git a/src/intl/locales/en/keywords.json b/src/intl/locales/en/keywords.json index 366a311f..d77521c2 100644 --- a/src/intl/locales/en/keywords.json +++ b/src/intl/locales/en/keywords.json @@ -1,7 +1,7 @@ { "dogma": "Discours.io, dogma, editorial principles, code of ethics, journalism, community", "guide": "discours.io, guide, help, how to start, reference, tutorial", - "": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography", + "home": "Discours.io, Discours magazine, Discours, culture, science, art, society, independent journalism, literature, music, cinema, video, photography", "principles": "Discours.io, communities, values, editorial rules, polyphony, creation", "terms-of-use": "Discours.io, site rules, terms of use", "topic": "{topic}, Discours.io, articles, journalism, research" diff --git a/src/intl/locales/ru/keywords.json b/src/intl/locales/ru/keywords.json index b542b3c1..a2c7ca63 100644 --- a/src/intl/locales/ru/keywords.json +++ b/src/intl/locales/ru/keywords.json @@ -1,7 +1,7 @@ { "dogma": "discours.io, догма, принципы редактирования, этический кодекс, журналистика, сообщество", "guide": "discours.io, гид, помощь, как начать, справочник, туториал", - "": "discours.io, Дискурс журнал, Дискурс, культура, наука, искусство, общество, независимая журналистика, литература, музыка, кино, видео, фотография", + "home": "discours.io, Дискурс журнал, Дискурс, культура, наука, искусство, общество, независимая журналистика, литература, музыка, кино, видео, фотография", "principles": "discours.io, сообщества, ценности, принципы редактировани, плюрализм мнений, сотворчество", "terms-of-use": "discours.io, правила сайта, правила, пользовательское соглашение", "topic": "discours.io, Дискурс, статьи, журналистика, исследование" diff --git a/src/routes/(home).tsx b/src/routes/(home).tsx index d7340e40..f406eb45 100644 --- a/src/routes/(home).tsx +++ b/src/routes/(home).tsx @@ -114,7 +114,7 @@ export default function HomePage(props: RouteSectionProps) { onMount(async () => await loadMoreFeatured()) return ( - + }> diff --git a/src/routes/[slug].tsx b/src/routes/[slug].tsx index 04f11939..1007c677 100644 --- a/src/routes/[slug].tsx +++ b/src/routes/[slug].tsx @@ -15,6 +15,7 @@ import { useLocalize } from '~/context/localize' import { getShout } from '~/graphql/api/public' import type { 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' @@ -91,9 +92,9 @@ export default (props: RouteSectionProps<{ article: Shout }>) => { }> setScrollToComments(value)} > diff --git a/src/routes/author/(all-authors).tsx b/src/routes/author/(all-authors).tsx index 55c4015a..8b0d6aa1 100644 --- a/src/routes/author/(all-authors).tsx +++ b/src/routes/author/(all-authors).tsx @@ -2,6 +2,7 @@ import { RouteDefinition, RouteLoadFuncArgs, type RouteSectionProps, createAsync import { Suspense, createEffect, on } from 'solid-js' import { AllAuthors } from '~/components/Views/AllAuthors' import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors' +import styles from '~/components/Views/AllAuthors/AllAuthors.module.scss' import { Loading } from '~/components/_shared/Loading' import { PageLayout } from '~/components/_shared/PageLayout' import { useAuthors } from '~/context/authors' @@ -27,13 +28,13 @@ export const route = { const isAll = !by || by === 'name' return { authors: isAll && (await fetchAllAuthors()), - topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'), - topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts') + authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'), + authorsByShouts: await fetchAuthorsWithStat(10, 'shouts') } as AllAuthorsData } } satisfies RouteDefinition -type AllAuthorsData = { authors: Author[]; topFollowedAuthors: Author[]; topShoutsAuthors: Author[] } +type AllAuthorsData = { authors: Author[]; authorsByFollowers: Author[]; authorsByShouts: Author[] } // addAuthors to context @@ -46,8 +47,8 @@ export default function AllAuthorsPage(props: RouteSectionProps) if (props.data) return props.data return { authors: await fetchAllAuthors(), - topFollowedAuthors: await fetchAuthorsWithStat(10, 'followers'), - topShoutsAuthors: await fetchAuthorsWithStat(10, 'shouts') + authorsByFollowers: await fetchAuthorsWithStat(10, 'followers'), + authorsByShouts: await fetchAuthorsWithStat(10, 'shouts') } as AllAuthorsData }) @@ -58,8 +59,8 @@ export default function AllAuthorsPage(props: RouteSectionProps) ([data, aa]) => { if (data && aa) { aa(data.authors as Author[]) - aa(data.topFollowedAuthors as Author[]) - aa(data.topShoutsAuthors as Author[]) + aa(data.authorsByFollowers as Author[]) + aa(data.authorsByShouts as Author[]) console.debug('[routes.author] added all authors:', data.authors) } }, @@ -68,14 +69,19 @@ export default function AllAuthorsPage(props: RouteSectionProps) ) return ( - + }> diff --git a/src/routes/author/[slug]/[tab].tsx b/src/routes/author/[slug]/[tab].tsx index 29e2c1ad..fb150d0f 100644 --- a/src/routes/author/[slug]/[tab].tsx +++ b/src/routes/author/[slug]/[tab].tsx @@ -9,6 +9,7 @@ import { useLocalize } from '~/context/localize' import { ReactionsProvider } from '~/context/reactions' import { loadShouts } from '~/graphql/api/public' import { Author, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' +import { getImageUrl } from '~/lib/getImageUrl' import { SHOUTS_PER_PAGE } from '../../(home)' const fetchAuthorShouts = async (slug: string, offset?: number) => { @@ -47,6 +48,12 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => { }) } }) + + const cover = createMemo(() => + author()?.pic + ? getImageUrl(author()?.pic || '', { width: 1200 }) + : getImageUrl('production/image/logo_image.png') + ) return ( }> }> @@ -54,8 +61,8 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => { title={`${t('Discours')} :: ${title()}`} headerTitle={author()?.name || ''} slug={author()?.slug} - articleBody={author()?.about || author()?.bio || ''} - cover={author()?.pic || ''} + desc={author()?.about || author()?.bio || ''} + cover={cover()} > { - const { t, lang } = useLocalize() - const ogImage = getImageUrl('production/image/logo_image.png') - const ogTitle = createMemo(() => t('Choose a post type')) - const description = createMemo(() => - t('Participate in the Discours: share information, join the editorial team') - ) + const { t } = useLocalize() const client = useGraphQL() const navigate = useNavigate() const handleCreate = async (layout: LayoutType) => { @@ -32,17 +22,11 @@ export default () => { } } return ( - - - - - - - - - - - +

{t('Choose a post type')}

diff --git a/src/routes/expo/[layout].tsx b/src/routes/expo/[layout].tsx index ddbe746c..2bf092d9 100644 --- a/src/routes/expo/[layout].tsx +++ b/src/routes/expo/[layout].tsx @@ -1,6 +1,6 @@ import { Params, RouteSectionProps, createAsync, useParams } from '@solidjs/router' import { createEffect, createMemo, on } from 'solid-js' -import { Topics } from '~/components/Nav/Topics' +import { TopicsNav } from '~/components/Nav/TopicsNav' import { Expo } from '~/components/Views/Expo' import { PageLayout } from '~/components/_shared/PageLayout' import { useLocalize } from '~/context/localize' @@ -61,7 +61,7 @@ export default (props: RouteSectionProps) => { return ( - + ) diff --git a/src/routes/feed/[feed].tsx b/src/routes/feed/[feed].tsx index bd93cf03..28056a0b 100644 --- a/src/routes/feed/[feed].tsx +++ b/src/routes/feed/[feed].tsx @@ -125,7 +125,12 @@ export default (props: RouteSectionProps) => { } createEffect(() => setIsLoadMoreButtonVisible(offset() < (shouts()?.length || 0))) return ( - + diff --git a/src/routes/topic/(all-topics).tsx b/src/routes/topic/(all-topics).tsx index 35c10d9f..daf202f3 100644 --- a/src/routes/topic/(all-topics).tsx +++ b/src/routes/topic/(all-topics).tsx @@ -22,7 +22,12 @@ export default (props: RouteSectionProps<{ topics: Topic[] }>) => { const { addTopics } = useTopics() createEffect(() => addTopics(topics() || [])) return ( - + }> diff --git a/src/routes/topic/[slug].tsx b/src/routes/topic/[slug].tsx index f1554cb4..bbc6eb4d 100644 --- a/src/routes/topic/[slug].tsx +++ b/src/routes/topic/[slug].tsx @@ -9,6 +9,8 @@ import { ReactionsProvider } from '~/context/reactions' import { useTopics } from '~/context/topics' import { loadShouts } 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)' const fetchTopicShouts = async (slug: string, offset?: number) => { @@ -43,15 +45,26 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => { }) } }) + const desc = createMemo(() => + topic()?.body + ? getArticleDescription(topic()?.body || '') + : t('The most interesting publications on the topic', { topicName: title() }) + ) + const cover = createMemo(() => + topic()?.pic + ? getImageUrl(topic()?.pic || '', { width: 1200 }) + : getImageUrl('production/image/logo_image.png') + ) return ( }> }>