From e03971193e59cce0a0f8dc5a428e223ebb368355 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 07:52:39 +0300 Subject: [PATCH 1/8] url-backward-compat --- src/routes/[slug]/[...tab].tsx | 13 ++++++------- src/routes/articles/[topic]/[slug].tsx | 3 +++ 2 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 src/routes/articles/[topic]/[slug].tsx diff --git a/src/routes/[slug]/[...tab].tsx b/src/routes/[slug]/[...tab].tsx index 8f7ad784..b17bdab7 100644 --- a/src/routes/[slug]/[...tab].tsx +++ b/src/routes/[slug]/[...tab].tsx @@ -1,6 +1,6 @@ import { RouteDefinition, RouteSectionProps, createAsync, useLocation } from '@solidjs/router' import { HttpStatusCode } from '@solidjs/start' -import { ErrorBoundary, Show, Suspense, createEffect, createSignal, on, onMount } from 'solid-js' +import { ErrorBoundary, Show, Suspense, createEffect, on, onMount } from 'solid-js' import { FourOuFourView } from '~/components/Views/FourOuFour' import { Loading } from '~/components/_shared/Loading' import { gaIdentity } from '~/config' @@ -28,9 +28,9 @@ export const route: RouteDefinition = { }) } -type ArticlePageProps = { article?: Shout; comments?: Reaction[]; votes?: Reaction[]; author?: Author } +export type ArticlePageProps = { article?: Shout; comments?: Reaction[]; votes?: Reaction[]; author?: Author } -type SlugPageProps = { +export type SlugPageProps = { article?: Shout comments?: Reaction[] votes?: Reaction[] @@ -38,7 +38,7 @@ type SlugPageProps = { topics: Topic[] } -export default (props: RouteSectionProps) => { +export default function ArticlePage(props: RouteSectionProps) { if (props.params.slug.startsWith('@')) { console.debug('[routes] [slug]/[...tab] starts with @, render as author page') const patchedProps = { @@ -66,7 +66,6 @@ export default (props: RouteSectionProps) => { function ArticlePage(props: RouteSectionProps) { const loc = useLocation() const { t } = useLocalize() - const [scrollToComments, setScrollToComments] = createSignal(false) const data = createAsync(async () => props.data?.article || (await fetchShout(props.params.slug))) onMount(async () => { @@ -114,10 +113,9 @@ export default (props: RouteSectionProps) => { headerTitle={data()?.title || ''} slug={data()?.slug} cover={data()?.cover || ''} - scrollToComments={(value) => setScrollToComments(value)} > - + @@ -127,3 +125,4 @@ export default (props: RouteSectionProps) => { } return } + diff --git a/src/routes/articles/[topic]/[slug].tsx b/src/routes/articles/[topic]/[slug].tsx new file mode 100644 index 00000000..98ee54f5 --- /dev/null +++ b/src/routes/articles/[topic]/[slug].tsx @@ -0,0 +1,3 @@ +import ArticlePage from "~/routes/[slug]/[...tab]" + +export default ArticlePage From 1ec368eae7a89320425bd14de969b46fa3d8a991 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 07:55:57 +0300 Subject: [PATCH 2/8] minor fixes --- public/icons/ediitor-bold.svg | 11 ----------- src/components/Views/ConnectView.tsx | 3 +-- src/components/Views/Profile/ProfileSettings.tsx | 6 +++--- 3 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 public/icons/ediitor-bold.svg diff --git a/public/icons/ediitor-bold.svg b/public/icons/ediitor-bold.svg deleted file mode 100644 index 144d4ead..00000000 --- a/public/icons/ediitor-bold.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/src/components/Views/ConnectView.tsx b/src/components/Views/ConnectView.tsx index 3aec4e40..0ed0a5af 100644 --- a/src/components/Views/ConnectView.tsx +++ b/src/components/Views/ConnectView.tsx @@ -1,5 +1,4 @@ -import { createSignal } from 'solid-js' -import { Show } from 'solid-js/web' +import { Show, createSignal } from 'solid-js' import { useLocalize } from '~/context/localize' export const ConnectView = () => { diff --git a/src/components/Views/Profile/ProfileSettings.tsx b/src/components/Views/Profile/ProfileSettings.tsx index 8b049d5e..e5f7715f 100644 --- a/src/components/Views/Profile/ProfileSettings.tsx +++ b/src/components/Views/Profile/ProfileSettings.tsx @@ -339,15 +339,15 @@ export const ProfileSettings = () => { maxLength={120} /> -

{t('About the author')}

+

{t('About')}

updateFormField('about', value)} From 260b95f692a365b83f73c06aa5ed1c21c3e37c4c Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 08:13:24 +0300 Subject: [PATCH 3/8] scrollto+shoutreaction --- src/components/Article/Comment/Comment.tsx | 6 ++-- .../Article/CommentRatingControl.tsx | 6 ++-- src/components/Article/CommentsTree.tsx | 4 +-- src/components/Article/FullArticle.tsx | 35 ++++++++----------- src/components/Article/ShoutRatingControl.tsx | 12 +++---- .../Feed/ArticleCard/ArticleCard.tsx | 8 ++--- src/components/HeaderNav/Header.tsx | 13 ++----- src/components/Views/Author/Author.tsx | 4 +-- src/components/_shared/PageLayout.tsx | 14 +++----- src/context/reactions.tsx | 33 +++++++++-------- src/routes/[slug]/[...tab].tsx | 8 +++-- src/routes/articles/[topic]/[slug].tsx | 2 +- 12 files changed, 62 insertions(+), 83 deletions(-) diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index 5bb4b6e8..039701d3 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -47,7 +47,7 @@ export const Comment = (props: Props) => { const [editedBody, setEditedBody] = createSignal() const { session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const { createReaction, updateReaction } = useReactions() + const { createShoutReaction, updateShoutReaction } = useReactions() const { showConfirm } = useUI() const { showSnackbar } = useSnackbar() const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) @@ -99,7 +99,7 @@ export const Comment = (props: Props) => { const handleCreate = async (value: string) => { try { setLoading(true) - await createReaction({ + await createShoutReaction({ reaction: { kind: ReactionKind.Comment, reply_to: props.comment.id, @@ -123,7 +123,7 @@ export const Comment = (props: Props) => { const handleUpdate = async (value: string) => { setLoading(true) try { - const reaction = await updateReaction({ + const reaction = await updateShoutReaction({ reaction: { id: props.comment.id || 0, kind: ReactionKind.Comment, diff --git a/src/components/Article/CommentRatingControl.tsx b/src/components/Article/CommentRatingControl.tsx index 08c8d555..ec0184bb 100644 --- a/src/components/Article/CommentRatingControl.tsx +++ b/src/components/Article/CommentRatingControl.tsx @@ -22,7 +22,7 @@ export const CommentRatingControl = (props: Props) => { const { session } = useSession() const uid = createMemo(() => session()?.user?.app_data?.profile?.id || 0) const { showSnackbar } = useSnackbar() - const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions() const checkReaction = (reactionKind: ReactionKind) => Object.values(reactionEntities).some( @@ -53,7 +53,7 @@ export const CommentRatingControl = (props: Props) => { r.shout.id === props.comment.shout.id && r.reply_to === props.comment.id ) - if (reactionToDelete) return deleteReaction(reactionToDelete.id) + if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id) } const handleRatingChange = async (isUpvote: boolean) => { @@ -63,7 +63,7 @@ export const CommentRatingControl = (props: Props) => { } else if (isDownvoted()) { await deleteCommentReaction(ReactionKind.Dislike) } else { - await createReaction({ + await createShoutReaction({ reaction: { kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, shout: props.comment.shout.id, diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 299c62a1..f10190f0 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -29,7 +29,7 @@ export const CommentsTree = (props: Props) => { const [newReactions, setNewReactions] = createSignal([]) const [clearEditor, setClearEditor] = createSignal(false) const [clickedReplyId, setClickedReplyId] = createSignal() - const { reactionEntities, createReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions() const comments = createMemo(() => Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') @@ -74,7 +74,7 @@ export const CommentsTree = (props: Props) => { const handleSubmitComment = async (value: string) => { setPosting(true) try { - await createReaction({ + await createShoutReaction({ reaction: { kind: ReactionKind.Comment, body: value, diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 014288ef..9c598a01 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -38,7 +38,6 @@ import { ShoutRatingControl } from './ShoutRatingControl' type Props = { article: Shout - scrollToComments?: boolean } type IframeSize = { @@ -47,8 +46,7 @@ type IframeSize = { } export type ArticlePageSearchParams = { - scrollTo: 'comments' - commentId: string + commentId?: string slide?: string } @@ -67,7 +65,7 @@ export const COMMENTS_PER_PAGE = 30 const VOTES_PER_PAGE = 50 export const FullArticle = (props: Props) => { - const [searchParams, changeSearchParams] = useSearchParams() + const [searchParams] = useSearchParams() const { showModal } = useUI() const { loadReactionsBy } = useReactions() const [selectedImage, setSelectedImage] = createSignal('') @@ -83,18 +81,20 @@ export const FullArticle = (props: Props) => { createEffect( on( pages, - async (p: Record) => { - await loadReactionsBy({ + (p: Record) => { + console.debug('content paginated') + loadReactionsBy({ by: { shout: props.article.slug, comment: true }, limit: COMMENTS_PER_PAGE, offset: COMMENTS_PER_PAGE * p.comments || 0 }) - await loadReactionsBy({ + loadReactionsBy({ by: { shout: props.article.slug, rating: true }, limit: VOTES_PER_PAGE, offset: VOTES_PER_PAGE * p.rating || 0 }) setIsReactionsLoaded(true) + console.debug('reactions paginated') }, { defer: true } ) @@ -165,15 +165,16 @@ export const FullArticle = (props: Props) => { const media = createMemo(() => JSON.parse(props.article.media || '[]')) let commentsRef: HTMLDivElement | undefined - createEffect(() => { if (searchParams?.commentId && isReactionsLoaded()) { - const commentElement = document.querySelector( - `[id='comment_${searchParams?.commentId}']` - ) + console.debug('comment id is in link, scroll to') + const scrollToElement = + document.querySelector(`[id='comment_${searchParams?.commentId}']`) || + commentsRef || + document.body - if (commentElement) { - requestAnimationFrame(() => scrollTo(commentElement)) + if (scrollToElement) { + requestAnimationFrame(() => scrollTo(scrollToElement)) } } }) @@ -316,14 +317,6 @@ export const FullArticle = (props: Props) => { onCleanup(() => window.removeEventListener('resize', updateIframeSizes)) }) - createEffect(() => props.scrollToComments && commentsRef && scrollTo(commentsRef)) - createEffect(() => { - if (searchParams?.scrollTo === 'comments' && commentsRef) { - requestAnimationFrame(() => commentsRef && scrollTo(commentsRef)) - changeSearchParams({ scrollTo: undefined }) - } - }) - const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` })) const getAuthorName = (a: Author) => lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name diff --git a/src/components/Article/ShoutRatingControl.tsx b/src/components/Article/ShoutRatingControl.tsx index b1302ed1..8df42e69 100644 --- a/src/components/Article/ShoutRatingControl.tsx +++ b/src/components/Article/ShoutRatingControl.tsx @@ -22,7 +22,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { const { loadShout } = useFeed() const { requireAuthentication, session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions() + const { reactionEntities, createShoutReaction, deleteShoutReaction, loadReactionsBy } = useReactions() const [isLoading, setIsLoading] = createSignal(false) const checkReaction = (reactionKind: ReactionKind) => @@ -43,7 +43,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { ) ) - const deleteShoutReaction = async (reactionKind: ReactionKind) => { + const removeReaction = async (reactionKind: ReactionKind) => { const reactionToDelete = Object.values(reactionEntities).find( (r) => r.kind === reactionKind && @@ -51,18 +51,18 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => { r.shout.id === props.shout.id && !r.reply_to ) - if (reactionToDelete) return deleteReaction(reactionToDelete.id) + if (reactionToDelete) return deleteShoutReaction(reactionToDelete.id) } const handleRatingChange = (isUpvote: boolean) => { requireAuthentication(async () => { setIsLoading(true) if (isUpvoted()) { - await deleteShoutReaction(ReactionKind.Like) + await removeReaction(ReactionKind.Like) } else if (isDownvoted()) { - await deleteShoutReaction(ReactionKind.Dislike) + await removeReaction(ReactionKind.Dislike) } else { - await createReaction({ + await createShoutReaction({ reaction: { kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, shout: props.shout.id diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 665a2d50..635fe7d2 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -1,4 +1,4 @@ -import { A, useNavigate, useSearchParams } from '@solidjs/router' +import { A, useNavigate } from '@solidjs/router' import { clsx } from 'clsx' import { Accessor, For, Show, createMemo, createSignal } from 'solid-js' import { Icon } from '~/components/_shared/Icon' @@ -105,7 +105,6 @@ export const ArticleCard = (props: ArticleCardProps) => { const { t, lang, formatDate } = useLocalize() const { session } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) - const [, changeSearchParams] = useSearchParams() const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false) const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true) @@ -129,10 +128,7 @@ export const ArticleCard = (props: ArticleCardProps) => { const scrollToComments = (event: MouseEvent & { currentTarget: HTMLAnchorElement; target: Element }) => { event.preventDefault() - changeSearchParams({ - scrollTo: 'comments' - }) - navigate(`/${props.article.slug}`) + navigate(`/${props.article.slug}?commentId=0`) } const onInvite = () => { diff --git a/src/components/HeaderNav/Header.tsx b/src/components/HeaderNav/Header.tsx index 6512dd01..a43584fa 100644 --- a/src/components/HeaderNav/Header.tsx +++ b/src/components/HeaderNav/Header.tsx @@ -23,7 +23,6 @@ type Props = { isHeaderFixed?: boolean desc?: string cover?: string - scrollToComments?: (value: boolean) => void } type HeaderSearchParams = { @@ -38,7 +37,7 @@ export const Header = (props: Props) => { const { t, lang } = useLocalize() const { modal } = useUI() const { requireAuthentication } = useSession() - const [searchParams] = useSearchParams() + const [searchParams, changeSearchParams] = useSearchParams() const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false) const [fixed, setFixed] = createSignal(false) @@ -85,14 +84,6 @@ export const Header = (props: Props) => { }) }) - const scrollToComments = ( - event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }, - value: boolean - ) => { - event.preventDefault() - props.scrollToComments?.(value) - } - const handleBookmarkButtonClick = (ev: { preventDefault: () => void }) => { requireAuthentication(() => { // TODO: implement bookmark clicked @@ -320,7 +311,7 @@ export const Header = (props: Props) => { } /> -
scrollToComments(event, true)} class={styles.control}> +
changeSearchParams({ commentId: 0 })} class={styles.control}>
diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index b534aa44..1deaa13b 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -175,7 +175,7 @@ export const AuthorView = (props: AuthorViewProps) => { const [loadMoreCommentsHidden, setLoadMoreCommentsHidden] = createSignal( Boolean(props.author?.stat && props.author?.stat?.comments === 0) ) - const { commentsByAuthor, addReactions } = useReactions() + const { commentsByAuthor, addShoutReactions } = useReactions() const loadMoreComments = async () => { if (!author()) return [] as LoadMoreItems saveScrollPosition() @@ -189,7 +189,7 @@ export const AuthorView = (props: AuthorViewProps) => { offset: commentsByAuthor()[aid]?.length || 0 }) const result = await authorCommentsFetcher() - result && addReactions(result) + result && addShoutReactions(result) restoreScrollPosition() return result as LoadMoreItems } diff --git a/src/components/_shared/PageLayout.tsx b/src/components/_shared/PageLayout.tsx index b27ae4d4..1ae5d22c 100644 --- a/src/components/_shared/PageLayout.tsx +++ b/src/components/_shared/PageLayout.tsx @@ -2,7 +2,7 @@ import { Meta, Title } from '@solidjs/meta' import { useLocation } from '@solidjs/router' import { clsx } from 'clsx' import type { JSX } from 'solid-js' -import { Show, createEffect, createMemo, createSignal } from 'solid-js' +import { Show, createMemo } from 'solid-js' import { useLocalize } from '~/context/localize' import { Shout } from '~/graphql/schema/core.gen' import enKeywords from '~/intl/locales/en/keywords.json' @@ -27,7 +27,6 @@ type PageLayoutProps = { class?: string withPadding?: boolean zeroBottomPadding?: boolean - scrollToComments?: (value: boolean) => void key?: string } @@ -48,12 +47,10 @@ export const PageLayout = (props: PageLayoutProps) => { : imageUrl ) const description = createMemo(() => props.desc || (props.article && descFromBody(props.article.body))) - const keypath = createMemo(() => (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords) - const keywords = createMemo( - () => props.keywords || (lang() === 'ru' ? ruKeywords[keypath()] : enKeywords[keypath()]) - ) - const [scrollToComments, setScrollToComments] = createSignal(false) - createEffect(() => props.scrollToComments?.(scrollToComments())) + const keywords = createMemo(() => { + const keypath = (props.key || loc?.pathname.split('/')[0]) as keyof typeof ruKeywords + return props.keywords || lang() === 'ru' ? ruKeywords[keypath] : enKeywords[keypath] + }) return ( <> {props.article?.title || t(props.title)} @@ -63,7 +60,6 @@ export const PageLayout = (props: PageLayoutProps) => { desc={props.desc} cover={imageUrl} isHeaderFixed={isHeaderFixed} - scrollToComments={(value) => setScrollToComments(value)} /> diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 02c572aa..4e559cc1 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -24,10 +24,10 @@ type ReactionsContextType = { reactionsByShout: Record commentsByAuthor: Accessor> loadReactionsBy: (args: QueryLoad_Reactions_ByArgs) => Promise - createReaction: (reaction: MutationCreate_ReactionArgs) => Promise - updateReaction: (reaction: MutationUpdate_ReactionArgs) => Promise - deleteReaction: (id: number) => Promise<{ error: string } | null> - addReactions: (rrr: Reaction[]) => void + createShoutReaction: (reaction: MutationCreate_ReactionArgs) => Promise + updateShoutReaction: (reaction: MutationUpdate_ReactionArgs) => Promise + deleteShoutReaction: (id: number) => Promise<{ error: string } | null> + addShoutReactions: (rrr: Reaction[]) => void } const ReactionsContext = createContext({} as ReactionsContextType) @@ -46,7 +46,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const { session } = useSession() const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const addReactions = (rrr: Reaction[]) => { + const addShoutReactions = (rrr: Reaction[]) => { const newReactionsByShout: Record = { ...reactionsByShout } const newReactionsByAuthor: Record = { ...reactionsByAuthor } const newReactionEntities = rrr.reduce( @@ -80,18 +80,16 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const fetcher = await loadReactions(opts) const result = (await fetcher()) || [] console.debug('[context.reactions] loaded', result) - result && addReactions(result) + result && addShoutReactions(result) return result } - const createReaction = async (input: MutationCreate_ReactionArgs): Promise => { + const createShoutReaction = async (input: MutationCreate_ReactionArgs): Promise => { const resp = await client()?.mutation(createReactionMutation, input).toPromise() const { error, reaction } = resp?.data?.create_reaction || {} if (error) await showSnackbar({ type: 'error', body: t(error) }) if (!reaction) return - const changes = { - [reaction.id]: reaction - } + const changes = { [reaction.id]: reaction } if ([ReactionKind.Like, ReactionKind.Dislike].includes(reaction.kind)) { const oppositeReactionKind = @@ -110,10 +108,11 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } } - setReactionEntities(changes) + addShoutReactions([reaction]) + return reaction } - const deleteReaction = async ( + const deleteShoutReaction = async ( reaction_id: number ): Promise<{ error: string; reaction?: string } | null> => { if (reaction_id) { @@ -129,7 +128,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { return null } - const updateReaction = async (input: MutationUpdate_ReactionArgs): Promise => { + const updateShoutReaction = async (input: MutationUpdate_ReactionArgs): Promise => { const resp = await client()?.mutation(updateReactionMutation, input).toPromise() const result = resp?.data?.update_reaction if (!result) throw new Error('cannot update reaction') @@ -143,10 +142,10 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const actions = { loadReactionsBy, - createReaction, - updateReaction, - deleteReaction, - addReactions + createShoutReaction, + updateShoutReaction, + deleteShoutReaction, + addShoutReactions } const value: ReactionsContextType = { reactionEntities, reactionsByShout, commentsByAuthor, ...actions } diff --git a/src/routes/[slug]/[...tab].tsx b/src/routes/[slug]/[...tab].tsx index b17bdab7..ef9fc842 100644 --- a/src/routes/[slug]/[...tab].tsx +++ b/src/routes/[slug]/[...tab].tsx @@ -28,7 +28,12 @@ export const route: RouteDefinition = { }) } -export type ArticlePageProps = { article?: Shout; comments?: Reaction[]; votes?: Reaction[]; author?: Author } +export type ArticlePageProps = { + article?: Shout + comments?: Reaction[] + votes?: Reaction[] + author?: Author +} export type SlugPageProps = { article?: Shout @@ -125,4 +130,3 @@ export default function ArticlePage(props: RouteSectionProps) { } return } - diff --git a/src/routes/articles/[topic]/[slug].tsx b/src/routes/articles/[topic]/[slug].tsx index 98ee54f5..0f23a54e 100644 --- a/src/routes/articles/[topic]/[slug].tsx +++ b/src/routes/articles/[topic]/[slug].tsx @@ -1,3 +1,3 @@ -import ArticlePage from "~/routes/[slug]/[...tab]" +import ArticlePage from '~/routes/[slug]/[...tab]' export default ArticlePage From 6ec271fe7c386a2eedbaaec58571e874a0ff0807 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 08:27:48 +0300 Subject: [PATCH 4/8] reactions-store-fix --- src/context/reactions.tsx | 95 +++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 4e559cc1..beac6688 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -1,5 +1,4 @@ import type { Accessor, JSX } from 'solid-js' - import { createContext, createMemo, createSignal, onCleanup, useContext } from 'solid-js' import { createStore, reconcile } from 'solid-js/store' import { coreApiUrl } from '~/config' @@ -47,23 +46,28 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const addShoutReactions = (rrr: Reaction[]) => { - const newReactionsByShout: Record = { ...reactionsByShout } - const newReactionsByAuthor: Record = { ...reactionsByAuthor } const newReactionEntities = rrr.reduce( - (acc: { [reaction_id: number]: Reaction }, reaction: Reaction) => { + (acc: Record, reaction: Reaction) => { acc[reaction.id] = reaction - if (!newReactionsByShout[reaction.shout.id]) newReactionsByShout[reaction.shout.id] = [] - newReactionsByShout[reaction.shout.id].push(reaction) - if (!newReactionsByAuthor[reaction.created_by.id]) newReactionsByAuthor[reaction.created_by.id] = [] - newReactionsByAuthor[reaction.created_by.id].push(reaction) return acc }, { ...reactionEntities } ) - setReactionEntities(newReactionEntities) - setReactionsByShout(newReactionsByShout) - setReactionsByAuthor(newReactionsByAuthor) + const newReactionsByShout = { ...reactionsByShout } + const newReactionsByAuthor = { ...reactionsByAuthor } + + rrr.forEach((reaction) => { + if (!newReactionsByShout[reaction.shout.id]) newReactionsByShout[reaction.shout.id] = [] + newReactionsByShout[reaction.shout.id].push(reaction) + + if (!newReactionsByAuthor[reaction.created_by.id]) newReactionsByAuthor[reaction.created_by.id] = [] + newReactionsByAuthor[reaction.created_by.id].push(reaction) + }) + + setReactionEntities(reconcile(newReactionEntities)) + setReactionsByShout(reconcile(newReactionsByShout)) + setReactionsByAuthor(reconcile(newReactionsByAuthor)) const newCommentsByAuthor = Object.fromEntries( Object.entries(newReactionsByAuthor).map(([authorId, reactions]) => [ @@ -76,11 +80,11 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { } const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise => { - !opts.by && console.warn('reactions provider got wrong opts') + if (!opts.by) console.warn('reactions provider got wrong opts') const fetcher = await loadReactions(opts) const result = (await fetcher()) || [] console.debug('[context.reactions] loaded', result) - result && addShoutReactions(result) + if (result) addShoutReactions(result) return result } @@ -89,27 +93,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const { error, reaction } = resp?.data?.create_reaction || {} if (error) await showSnackbar({ type: 'error', body: t(error) }) if (!reaction) return - const changes = { [reaction.id]: reaction } - - if ([ReactionKind.Like, ReactionKind.Dislike].includes(reaction.kind)) { - const oppositeReactionKind = - reaction.kind === ReactionKind.Like ? ReactionKind.Dislike : ReactionKind.Like - - const oppositeReaction = Object.values(reactionEntities).find( - (r) => - r.kind === oppositeReactionKind && - r.created_by.slug === reaction.created_by.slug && - r.shout.id === reaction.shout.id && - r.reply_to === reaction.reply_to - ) - - if (oppositeReaction) { - changes[oppositeReaction.id] = undefined - } - } - addShoutReactions([reaction]) - return reaction } const deleteShoutReaction = async ( @@ -118,11 +102,41 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { if (reaction_id) { const resp = await client()?.mutation(destroyReactionMutation, { reaction_id }).toPromise() const result = resp?.data?.destroy_reaction + if (!result.error) { - setReactionEntities({ - [reaction_id]: undefined - }) + // Находим реакцию, которую нужно удалить + const reactionToDelete = reactionEntities[reaction_id] + + if (reactionToDelete) { + // Удаляем из reactionEntities + const newReactionEntities = { ...reactionEntities } + delete newReactionEntities[reaction_id] + + // Удаляем из reactionsByShout + const newReactionsByShout = { ...reactionsByShout } + const shoutReactions = newReactionsByShout[reactionToDelete.shout.id] + if (shoutReactions) { + newReactionsByShout[reactionToDelete.shout.id] = shoutReactions.filter( + (r) => r.id !== reaction_id + ) + } + + // Удаляем из reactionsByAuthor + const newReactionsByAuthor = { ...reactionsByAuthor } + const authorReactions = newReactionsByAuthor[reactionToDelete.created_by.id] + if (authorReactions) { + newReactionsByAuthor[reactionToDelete.created_by.id] = authorReactions.filter( + (r) => r.id !== reaction_id + ) + } + + // Обновляем стои с использованием reconcile + setReactionEntities(reconcile(newReactionEntities)) + setReactionsByShout(reconcile(newReactionsByShout)) + setReactionsByAuthor(reconcile(newReactionsByAuthor)) + } } + return result } return null @@ -134,7 +148,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { if (!result) throw new Error('cannot update reaction') const { error, reaction } = result if (error) await showSnackbar({ type: 'error', body: t(error) }) - if (reaction) setReactionEntities(reaction.id, reaction) + if (reaction) setReactionEntities(reaction.id, reaction) // use setter to update store return reaction } @@ -148,7 +162,12 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { addShoutReactions } - const value: ReactionsContextType = { reactionEntities, reactionsByShout, commentsByAuthor, ...actions } + const value: ReactionsContextType = { + reactionEntities, + reactionsByShout, + commentsByAuthor, + ...actions + } return {props.children} } From 8824fbab2f1036364ed315d0be9f1d7ab0e7508a Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 6 Sep 2024 08:33:44 +0300 Subject: [PATCH 5/8] reactions-store-fix --- src/context/reactions.tsx | 61 +++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index beac6688..911d7352 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -1,6 +1,5 @@ import type { Accessor, JSX } from 'solid-js' import { createContext, createMemo, createSignal, onCleanup, useContext } from 'solid-js' -import { createStore, reconcile } from 'solid-js/store' import { coreApiUrl } from '~/config' import { loadReactions } from '~/graphql/api/public' import createReactionMutation from '~/graphql/mutation/core/reaction-create' @@ -19,8 +18,8 @@ import { useSession } from './session' import { useSnackbar } from './ui' type ReactionsContextType = { - reactionEntities: Record - reactionsByShout: Record + reactionEntities: Accessor> + reactionsByShout: Accessor> commentsByAuthor: Accessor> loadReactionsBy: (args: QueryLoad_Reactions_ByArgs) => Promise createShoutReaction: (reaction: MutationCreate_ReactionArgs) => Promise @@ -36,9 +35,9 @@ export function useReactions() { } export const ReactionsProvider = (props: { children: JSX.Element }) => { - const [reactionEntities, setReactionEntities] = createStore>({}) - const [reactionsByShout, setReactionsByShout] = createStore>({}) - const [reactionsByAuthor, setReactionsByAuthor] = createStore>({}) + const [reactionEntities, setReactionEntities] = createSignal>({}) + const [reactionsByShout, setReactionsByShout] = createSignal>({}) + const [reactionsByAuthor, setReactionsByAuthor] = createSignal>({}) const [commentsByAuthor, setCommentsByAuthor] = createSignal>({}) const { t } = useLocalize() const { showSnackbar } = useSnackbar() @@ -46,18 +45,13 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const addShoutReactions = (rrr: Reaction[]) => { - const newReactionEntities = rrr.reduce( - (acc: Record, reaction: Reaction) => { - acc[reaction.id] = reaction - return acc - }, - { ...reactionEntities } - ) - - const newReactionsByShout = { ...reactionsByShout } - const newReactionsByAuthor = { ...reactionsByAuthor } + const newReactionEntities = { ...reactionEntities() } + const newReactionsByShout = { ...reactionsByShout() } + const newReactionsByAuthor = { ...reactionsByAuthor() } rrr.forEach((reaction) => { + newReactionEntities[reaction.id] = reaction + if (!newReactionsByShout[reaction.shout.id]) newReactionsByShout[reaction.shout.id] = [] newReactionsByShout[reaction.shout.id].push(reaction) @@ -65,14 +59,14 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { newReactionsByAuthor[reaction.created_by.id].push(reaction) }) - setReactionEntities(reconcile(newReactionEntities)) - setReactionsByShout(reconcile(newReactionsByShout)) - setReactionsByAuthor(reconcile(newReactionsByAuthor)) + setReactionEntities(newReactionEntities) + setReactionsByShout(newReactionsByShout) + setReactionsByAuthor(newReactionsByAuthor) const newCommentsByAuthor = Object.fromEntries( Object.entries(newReactionsByAuthor).map(([authorId, reactions]) => [ authorId, - reactions.filter((x: Reaction) => x.kind === ReactionKind.Comment) + reactions.filter((x) => x.kind === ReactionKind.Comment) ]) ) @@ -104,16 +98,13 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const result = resp?.data?.destroy_reaction if (!result.error) { - // Находим реакцию, которую нужно удалить - const reactionToDelete = reactionEntities[reaction_id] + const reactionToDelete = reactionEntities()[reaction_id] if (reactionToDelete) { - // Удаляем из reactionEntities - const newReactionEntities = { ...reactionEntities } + const newReactionEntities = { ...reactionEntities() } delete newReactionEntities[reaction_id] - // Удаляем из reactionsByShout - const newReactionsByShout = { ...reactionsByShout } + const newReactionsByShout = { ...reactionsByShout() } const shoutReactions = newReactionsByShout[reactionToDelete.shout.id] if (shoutReactions) { newReactionsByShout[reactionToDelete.shout.id] = shoutReactions.filter( @@ -121,8 +112,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { ) } - // Удаляем из reactionsByAuthor - const newReactionsByAuthor = { ...reactionsByAuthor } + const newReactionsByAuthor = { ...reactionsByAuthor() } const authorReactions = newReactionsByAuthor[reactionToDelete.created_by.id] if (authorReactions) { newReactionsByAuthor[reactionToDelete.created_by.id] = authorReactions.filter( @@ -130,10 +120,9 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { ) } - // Обновляем стои с использованием reconcile - setReactionEntities(reconcile(newReactionEntities)) - setReactionsByShout(reconcile(newReactionsByShout)) - setReactionsByAuthor(reconcile(newReactionsByAuthor)) + setReactionEntities(newReactionEntities) + setReactionsByShout(newReactionsByShout) + setReactionsByAuthor(newReactionsByAuthor) } } @@ -148,11 +137,15 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { if (!result) throw new Error('cannot update reaction') const { error, reaction } = result if (error) await showSnackbar({ type: 'error', body: t(error) }) - if (reaction) setReactionEntities(reaction.id, reaction) // use setter to update store + if (reaction) { + const newReactionEntities = { ...reactionEntities() } + newReactionEntities[reaction.id] = reaction + setReactionEntities(newReactionEntities) + } return reaction } - onCleanup(() => setReactionEntities(reconcile({}))) + onCleanup(() => setReactionEntities({})) const actions = { loadReactionsBy, From ad4bda3c247d38edd8c32ff880577094d312cc3d Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 15 Sep 2024 19:41:02 +0300 Subject: [PATCH 6/8] prestorybook --- .storybook/main.ts | 12 +- .storybook/preview.ts | 1 - api/jsonify.js | 38 ++++ app.config.ts | 52 +---- biome.json | 1 + package.json | 11 +- src/app.tsx | 7 +- src/components/Article/CommentsTree.tsx | 6 +- src/components/Article/ShoutRatingControl.tsx | 4 +- src/components/Editor/Editor.tsx | 31 +-- .../Editor/InsertLinkForm/InsertLinkForm.tsx | 3 +- src/components/Editor/SimplifiedEditor.tsx | 200 ++++++++++-------- src/components/Editor/extensions/Article.ts | 4 +- .../Views/PublishSettings/PublishSettings.tsx | 5 +- src/context/authors.tsx | 7 +- vite.config.ts | 51 +++++ 16 files changed, 245 insertions(+), 188 deletions(-) create mode 100644 api/jsonify.js create mode 100644 vite.config.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 689d29fb..0d6462d6 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -7,19 +7,27 @@ const config: StorybookConfig = { '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-a11y', - '@storybook/addon-themes' + '@storybook/addon-themes', + '@storybook/addon-style-config' ], framework: { name: 'storybook-solidjs-vite', options: { builder: { - viteConfigPath: './app.config.ts' + viteConfigPath: './vite.config.ts' } } as FrameworkOptions }, docs: { autodocs: 'tag' }, + viteFinal: (config) => { + if (config.build) { + config.build.sourcemap = true + config.build.minify = process.env.NODE_ENV === 'production' + } + return config + }, previewHead: (head) => ` ${head}