diff --git a/src/app/(page-detail)/[slug]/loading.tsx b/src/app/(page-detail)/[slug]/loading.tsx index 4374b05750..a3b5b526af 100644 --- a/src/app/(page-detail)/[slug]/loading.tsx +++ b/src/app/(page-detail)/[slug]/loading.tsx @@ -1,7 +1,9 @@ import { Loading } from '~/components/ui/loading' -export default () => ( -
- -
-) +export default function LoadingPage() { + return ( +
+ +
+ ) +} diff --git a/src/app/(page-detail)/[slug]/page.tsx b/src/app/(page-detail)/[slug]/page.tsx index 70334ec17d..02d6440a4c 100644 --- a/src/app/(page-detail)/[slug]/page.tsx +++ b/src/app/(page-detail)/[slug]/page.tsx @@ -2,13 +2,13 @@ import { TocAside } from '~/components/widgets/toc' import { LayoutRightSidePortal } from '~/providers/shared/LayoutRightSideProvider' import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider' -import { MarkdownImageRecordProviderInternal, PostMarkdown } from './pageExtra' +import { MarkdownImageRecordProviderInternal, PageMarkdown } from './pageExtra' const PageDetail = () => { return ( - + diff --git a/src/app/(page-detail)/[slug]/pageExtra.tsx b/src/app/(page-detail)/[slug]/pageExtra.tsx index e95d7bb081..01ede0fc89 100644 --- a/src/app/(page-detail)/[slug]/pageExtra.tsx +++ b/src/app/(page-detail)/[slug]/pageExtra.tsx @@ -25,7 +25,7 @@ export const PageLoading: Component = ({ children }) => { return children } -export const PostMarkdown = () => { +export const PageMarkdown = () => { const text = useCurrentPageDataSelector((data) => data?.text) if (!text) return null diff --git a/src/app/notes/[id]/layout.tsx b/src/app/notes/[id]/layout.tsx index c6d5eae5a6..f3f723629b 100644 --- a/src/app/notes/[id]/layout.tsx +++ b/src/app/notes/[id]/layout.tsx @@ -18,7 +18,7 @@ import { import { CurrentNoteIdProvider } from '~/providers/note/CurrentNoteIdProvider' import { queries } from '~/queries/definition' -import { Paper } from '../Paper' +import { Paper } from '../../../components/layout/container/Paper' import { Transition } from './Transtion' export const generateMetadata = async ({ diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index 070635b3e5..5ab81f8026 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -3,34 +3,28 @@ 'use client' import { memo, useEffect } from 'react' -import { Balancer } from 'react-wrap-balancer' -import clsx from 'clsx' -import dayjs from 'dayjs' import dynamic from 'next/dynamic' -import type { Image } from '@mx-space/api-client' -import type { MarkdownToJSX } from '~/components/ui/markdown' -import type { PropsWithChildren } from 'react' import { ClientOnly } from '~/components/common/ClientOnly' -import { MdiClockOutline } from '~/components/icons/clock' -import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks' -import { FloatPopover } from '~/components/ui/float-popover' -import { Markdown } from '~/components/ui/markdown' import { NoteBanner } from '~/components/widgets/note/NoteBanner' import { ArticleRightAside } from '~/components/widgets/shared/ArticleRightAside' import { BanCopyWrapper } from '~/components/widgets/shared/BanCopyWrapper' import { XLogInfoForNote, XLogSummaryForNote } from '~/components/widgets/xlog' -import { parseDate } from '~/lib/datetime' -import { noopArr } from '~/lib/noop' -import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider' -import { useCurrentNoteDataSelector } from '~/providers/note/CurrentNoteDataProvider' +import { springScrollToTop } from '~/lib/scroller' import { useCurrentNoteId } from '~/providers/note/CurrentNoteIdProvider' import { LayoutRightSidePortal } from '~/providers/shared/LayoutRightSideProvider' import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider' import { NoteHideIfSecret } from '../../../components/widgets/note/NoteHideIfSecret' import { NoteMetaBar } from '../../../components/widgets/note/NoteMetaBar' -import styles from './page.module.css' +import { + IndentArticleContainer, + NoteHeaderDate, + NoteHeaderMetaInfoSetting, + NoteMarkdown, + NoteMarkdownImageRecordProvider, + NoteTitle, +} from './pageExtra' const NoteActionAside = dynamic(() => import('~/components/widgets/note/NoteActionAside').then( @@ -65,17 +59,16 @@ const PageImpl = () => { const NotePage = memo(function Notepage() { const noteId = useCurrentNoteId() + + useEffect(() => { + springScrollToTop() + }, [noteId]) + if (!noteId) return null return ( <> -
+
@@ -106,7 +99,7 @@ const NotePage = memo(function Notepage() { -
+ @@ -116,107 +109,4 @@ const NotePage = memo(function Notepage() { ) }) -const NoteTitle = () => { - const title = useCurrentNoteDataSelector((data) => data?.data.title) - if (!title) return null - return ( -

- {title} -

- ) -} - -const NoteDateMeta = () => { - const created = useCurrentNoteDataSelector((data) => data?.data.created) - if (!created) return null - const dateFormat = dayjs(created) - .locale('zh-cn') - .format('YYYY 年 M 月 D 日 dddd') - - return ( - - - - - ) -} - -const NoteHeaderDate = () => { - const date = useCurrentNoteDataSelector((data) => ({ - created: data?.data.created, - modified: data?.data.modified, - })) - if (!date?.created) return null - - const tips = `创建于 ${parseDate(date.created, 'YYYY 年 M 月 D 日 dddd')}${ - date.modified - ? `,修改于 ${parseDate(date.modified, 'YYYY 年 M 月 D 日 dddd')}` - : '' - }` - - return ( - - {tips} - - ) -} - -const NoteMarkdown = () => { - const text = useCurrentNoteDataSelector((data) => data?.data.text)! - - return ( - - ) -} -const NoteMarkdownImageRecordProvider = (props: PropsWithChildren) => { - const images = useCurrentNoteDataSelector( - (data) => data?.data.images || (noopArr as Image[]), - )! - - return ( - - {props.children} - - ) -} - -const NoteHeaderMetaInfoSetting = () => { - const setHeaderMetaInfo = useSetHeaderMetaInfo() - const meta = useCurrentNoteDataSelector((data) => { - if (!data) return null - const note = data.data - - return { - title: note?.title, - description: `手记${note.topic?.name ? ` / ${note.topic?.name}` : ''}`, - slug: note?.nid.toString(), - } - }) - - useEffect(() => { - if (meta) setHeaderMetaInfo(meta) - }, [meta]) - - return null -} - -const MarkdownRenderers: { [name: string]: Partial } = { - text: { - react(node, _, state) { - return ( - - {node.content} - - ) - }, - }, -} - export default PageImpl diff --git a/src/app/notes/[id]/pageExtra.tsx b/src/app/notes/[id]/pageExtra.tsx new file mode 100644 index 0000000000..83dab4ac7f --- /dev/null +++ b/src/app/notes/[id]/pageExtra.tsx @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +'use client' + +import { useEffect } from 'react' +import Balancer from 'react-wrap-balancer' +import clsx from 'clsx' +import dayjs from 'dayjs' +import type { Image } from '@mx-space/api-client' +import type { MarkdownToJSX } from '~/components/ui/markdown' +import type { PropsWithChildren } from 'react' + +import { MdiClockOutline } from '~/components/icons/clock' +import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks' +import { FloatPopover } from '~/components/ui/float-popover' +import { Markdown } from '~/components/ui/markdown' +import { parseDate } from '~/lib/datetime' +import { noopArr } from '~/lib/noop' +import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider' +import { useCurrentNoteDataSelector } from '~/providers/note/CurrentNoteDataProvider' + +import styles from './page.module.css' + +export const NoteTitle = () => { + const title = useCurrentNoteDataSelector((data) => data?.data.title) + if (!title) return null + return ( +

+ {title} +

+ ) +} + +export const NoteDateMeta = () => { + const created = useCurrentNoteDataSelector((data) => data?.data.created) + if (!created) return null + const dateFormat = dayjs(created) + .locale('zh-cn') + .format('YYYY 年 M 月 D 日 dddd') + + return ( + + + + + ) +} +export const NoteHeaderDate = () => { + const date = useCurrentNoteDataSelector((data) => ({ + created: data?.data.created, + modified: data?.data.modified, + })) + if (!date?.created) return null + + const tips = `创建于 ${parseDate(date.created, 'YYYY 年 M 月 D 日 dddd')}${ + date.modified + ? `,修改于 ${parseDate(date.modified, 'YYYY 年 M 月 D 日 dddd')}` + : '' + }` + + return ( + + {tips} + + ) +} +export const NoteMarkdown = () => { + const text = useCurrentNoteDataSelector((data) => data?.data.text)! + + return ( + + ) +} +export const NoteMarkdownImageRecordProvider = (props: PropsWithChildren) => { + const images = useCurrentNoteDataSelector( + (data) => data?.data.images || (noopArr as Image[]), + )! + + return ( + + {props.children} + + ) +} +export const NoteHeaderMetaInfoSetting = () => { + const setHeaderMetaInfo = useSetHeaderMetaInfo() + const meta = useCurrentNoteDataSelector((data) => { + if (!data) return null + const note = data.data + + return { + title: note?.title, + description: `手记${note.topic?.name ? ` / ${note.topic?.name}` : ''}`, + slug: note?.nid.toString(), + } + }) + + useEffect(() => { + if (meta) setHeaderMetaInfo(meta) + }, [meta]) + + return null +} +const MarkdownRenderers: { [name: string]: Partial } = { + text: { + react(node, _, state) { + return ( + + {node.content} + + ) + }, + }, +} +export const IndentArticleContainer = (props: PropsWithChildren) => { + return ( +
+ {props.children} +
+ ) +} diff --git a/src/app/notes/error.tsx b/src/app/notes/error.tsx index a26c072d59..b500066a07 100644 --- a/src/app/notes/error.tsx +++ b/src/app/notes/error.tsx @@ -11,7 +11,7 @@ import { NotePasswordForm } from '~/components/widgets/note/NotePasswordForm' import { isRequestError, pickStatusCode } from '~/lib/is-error' import { setCurrentNoteId } from '~/providers/note/CurrentNoteIdProvider' -import { Paper } from './Paper' +import { Paper } from '../../components/layout/container/Paper' // TODO Catch if 404 or 403 export default ({ error, reset }: { error: Error; reset: () => void }) => { diff --git a/src/app/notes/page.tsx b/src/app/notes/page.tsx index 5f8511c8d5..d0639efa04 100644 --- a/src/app/notes/page.tsx +++ b/src/app/notes/page.tsx @@ -9,7 +9,7 @@ import { apiClient } from '~/lib/request' import { routeBuilder, Routes } from '~/lib/route-builder' import { queries } from '~/queries/definition' -import { Paper } from './Paper' +import { Paper } from '../../components/layout/container/Paper' export default function Page() { const { data } = useQuery( diff --git a/src/app/posts/(post-detail)/[category]/[slug]/page.tsx b/src/app/posts/(post-detail)/[category]/[slug]/page.tsx index 49ea868c2d..d60782ae1d 100644 --- a/src/app/posts/(post-detail)/[category]/[slug]/page.tsx +++ b/src/app/posts/(post-detail)/[category]/[slug]/page.tsx @@ -2,28 +2,27 @@ import { useEffect } from 'react' import { Balancer } from 'react-wrap-balancer' -import type { Image } from '@mx-space/api-client' -import type { PropsWithChildren } from 'react' import { ClientOnly } from '~/components/common/ClientOnly' -import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks' -import { Markdown } from '~/components/ui/markdown' import { PostActionAside } from '~/components/widgets/post/PostActionAside' import { PostCopyright } from '~/components/widgets/post/PostCopyright' -import { PostMetaBar } from '~/components/widgets/post/PostMetaBar' import { PostOutdate } from '~/components/widgets/post/PostOutdate' import { PostRelated } from '~/components/widgets/post/PostRelated' import { ArticleRightAside } from '~/components/widgets/shared/ArticleRightAside' import { SubscribeBell } from '~/components/widgets/subscribe/SubscribeBell' import { XLogInfoForPost, XLogSummaryForPost } from '~/components/widgets/xlog' -import { noopArr } from '~/lib/noop' import { springScrollToTop } from '~/lib/scroller' -import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider' import { useCurrentPostDataSelector } from '~/providers/post/CurrentPostDataProvider' import { LayoutRightSidePortal } from '~/providers/shared/LayoutRightSideProvider' import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider' import Loading from './loading' +import { + HeaderMetaInfoSetting, + PostMarkdown, + PostMarkdownImageRecordProvider, + PostMetaBarInternal, +} from './pageExtra' const PostPage = () => { const id = useCurrentPostDataSelector((p) => p?.id) @@ -75,66 +74,4 @@ const PostPage = () => { ) } -const PostMarkdown = () => { - const text = useCurrentPostDataSelector((data) => data?.text) - if (!text) return null - - return ( - - ) -} -const PostMarkdownImageRecordProvider = (props: PropsWithChildren) => { - const images = useCurrentPostDataSelector( - (data) => data?.images || (noopArr as Image[]), - ) - if (!images) return null - - return ( - - {props.children} - - ) -} - -const HeaderMetaInfoSetting = () => { - const setHeaderMetaInfo = useSetHeaderMetaInfo() - const meta = useCurrentPostDataSelector((data) => { - if (!data) return null - - return { - title: data.title, - description: - data.category.name + - (data.tags.length > 0 ? ` / ${data.tags.join(', ')}` : ''), - slug: `${data.category.slug}/${data.slug}`, - } - }) - - useEffect(() => { - if (meta) setHeaderMetaInfo(meta) - }, [meta]) - - return null -} - -const PostMetaBarInternal: Component = ({ className }) => { - const meta = useCurrentPostDataSelector((data) => { - if (!data) return - return { - created: data.created, - category: data.category, - tags: data.tags, - count: data.count, - modified: data.modified, - } - }) - if (!meta) return null - return -} - export default PostPage diff --git a/src/app/posts/(post-detail)/[category]/[slug]/pageExtra.tsx b/src/app/posts/(post-detail)/[category]/[slug]/pageExtra.tsx new file mode 100644 index 0000000000..9fc74dbc3a --- /dev/null +++ b/src/app/posts/(post-detail)/[category]/[slug]/pageExtra.tsx @@ -0,0 +1,71 @@ +'use client' + +import { useEffect } from 'react' +import type { Image } from '@mx-space/api-client' +import type { PropsWithChildren } from 'react' + +import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks' +import { Markdown } from '~/components/ui/markdown' +import { PostMetaBar } from '~/components/widgets/post/PostMetaBar' +import { noopArr } from '~/lib/noop' +import { MarkdownImageRecordProvider } from '~/providers/article/MarkdownImageRecordProvider' +import { useCurrentPostDataSelector } from '~/providers/post/CurrentPostDataProvider' + +export const PostMarkdown = () => { + const text = useCurrentPostDataSelector((data) => data?.text) + if (!text) return null + + return ( + + ) +} +export const PostMarkdownImageRecordProvider = (props: PropsWithChildren) => { + const images = useCurrentPostDataSelector( + (data) => data?.images || (noopArr as Image[]), + ) + + return ( + + {props.children} + + ) +} +export const HeaderMetaInfoSetting = () => { + const setHeaderMetaInfo = useSetHeaderMetaInfo() + const meta = useCurrentPostDataSelector((data) => { + if (!data) return null + + return { + title: data.title, + description: + data.category.name + + (data.tags.length > 0 ? ` / ${data.tags.join(', ')}` : ''), + slug: `${data.category.slug}/${data.slug}`, + } + }) + + useEffect(() => { + if (meta) setHeaderMetaInfo(meta) + }, [meta]) + + return null +} +export const PostMetaBarInternal: Component = ({ className }) => { + const meta = useCurrentPostDataSelector((data) => { + if (!data) return + return { + created: data.created, + category: data.category, + tags: data.tags, + count: data.count, + modified: data.modified, + } + }) + if (!meta) return null + return +} diff --git a/src/app/timeline/page.tsx b/src/app/timeline/page.tsx index d459537feb..a8ea748d20 100644 --- a/src/app/timeline/page.tsx +++ b/src/app/timeline/page.tsx @@ -1,12 +1,13 @@ 'use client' import { useQuery } from '@tanstack/react-query' -import { useEffect } from 'react' +import { memo, useCallback, useEffect } from 'react' import clsx from 'clsx' import { m } from 'framer-motion' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import type { TimelineData } from '@mx-space/api-client' +import type { SyntheticEvent } from 'react' import { TimelineType } from '@mx-space/api-client' @@ -16,9 +17,13 @@ import { NormalContainer } from '~/components/layout/container/Normal' import { Divider } from '~/components/ui/divider' import { TimelineList } from '~/components/ui/list/TimelineList' import { BottomToUpSoftScaleTransitionView } from '~/components/ui/transition/BottomToUpSoftScaleTransitionView' +import { NotePreview } from '~/components/widgets/peek/NotePreview' +import { PeekModal } from '~/components/widgets/peek/PeekModal' +import { PostPreview } from '~/components/widgets/peek/PostPreview' import { TimelinProgress } from '~/components/widgets/timeline/TimelineProgress' import { apiClient } from '~/lib/request' import { springScrollToElement } from '~/lib/scroller' +import { useModalStack } from '~/providers/root/modal-stack-provider' enum ArticleType { Post, @@ -100,9 +105,6 @@ export default function TimelinePage() { }, }) - const router = useRouter() - const isMobile = useIsMobile() - useJumpTo() if (!data) return null @@ -203,44 +205,7 @@ export default function TimelinePage() { {value.map((item) => { - return ( -
  • - - - {Intl.DateTimeFormat('en-us', { - month: '2-digit', - day: '2-digit', - }).format(item.date)} - - - {item.title} - - {item.important && ( - { - const url = new URL(window.location.href) - url.searchParams.set('memory', 'true') - router.push(url.href) - }} - /> - )} - - {!isMobile && ( - - {item.meta.map((m, i) => (i === 0 ? m : `/${m}`))} - - )} -
  • - ) + return })}
    @@ -250,3 +215,87 @@ export default function TimelinePage() { ) } + +const Item = memo<{ + item: MapType +}>(({ item }) => { + const router = useRouter() + const isMobile = useIsMobile() + const { present } = useModalStack() + const handlePeek = useCallback((e: SyntheticEvent) => { + if (item.type === ArticleType.Note) { + { + e.preventDefault() + present({ + clickOutsideToDismiss: true, + title: 'Preview', + modalClassName: 'flex justify-center', + modalContainerClassName: 'flex justify-center', + CustomModalComponent: () => ( + + + + ), + content: () => null, + }) + } + } else if (item.type === ArticleType.Post) { + e.preventDefault() + const splitpath = item.href.split('/') + const slug = splitpath.pop()! + const category = splitpath.pop()! + present({ + clickOutsideToDismiss: true, + title: 'Preview', + modalClassName: 'flex justify-center', + modalContainerClassName: 'flex justify-center', + CustomModalComponent: () => ( + + + + ), + content: () => null, + }) + } + }, []) + return ( +
  • + + + {Intl.DateTimeFormat('en-us', { + month: '2-digit', + day: '2-digit', + }).format(item.date)} + + + {item.title} + + {item.important && ( + { + const url = new URL(window.location.href) + url.searchParams.set('memory', 'true') + router.push(url.href) + }} + /> + )} + + {!isMobile && ( + + {item.meta.map((m, i) => (i === 0 ? m : `/${m}`))} + + )} +
  • + ) +}) +Item.displayName = 'Item' diff --git a/src/app/notes/Paper.tsx b/src/components/layout/container/Paper.tsx similarity index 100% rename from src/app/notes/Paper.tsx rename to src/components/layout/container/Paper.tsx diff --git a/src/components/layout/content/Content.tsx b/src/components/layout/content/Content.tsx index 013cdf0f38..57b5cce36c 100644 --- a/src/components/layout/content/Content.tsx +++ b/src/components/layout/content/Content.tsx @@ -1,6 +1,6 @@ export const Content: Component = ({ children }) => { return ( -
    +
    {children}
    ) diff --git a/src/components/widgets/note/NoteHideIfSecret.tsx b/src/components/widgets/note/NoteHideIfSecret.tsx index b607d9b317..09c14e8019 100644 --- a/src/components/widgets/note/NoteHideIfSecret.tsx +++ b/src/components/widgets/note/NoteHideIfSecret.tsx @@ -8,11 +8,11 @@ import dayjs from 'dayjs' import { useIsLogged } from '~/atoms/owner' import { toast } from '~/lib/toast' import { useCurrentNoteDataSelector } from '~/providers/note/CurrentNoteDataProvider' -import { useCurrentNoteId } from '~/providers/note/CurrentNoteIdProvider' export const NoteHideIfSecret: Component = ({ children }) => { const noteSecret = useCurrentNoteDataSelector((data) => data?.data.secret) - const noteId = useCurrentNoteId() + + const noteId = useCurrentNoteDataSelector((data) => data?.data.nid) const secretDate = useMemo(() => new Date(noteSecret!), [noteSecret]) const isSecret = noteSecret ? dayjs(noteSecret).isAfter(new Date()) : false diff --git a/src/components/widgets/peek/NotePreview.tsx b/src/components/widgets/peek/NotePreview.tsx new file mode 100644 index 0000000000..c7dfcf3119 --- /dev/null +++ b/src/components/widgets/peek/NotePreview.tsx @@ -0,0 +1,74 @@ +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import { atom } from 'jotai' +import type { NoteWrappedPayload } from '@mx-space/api-client' +import type { FC } from 'react' + +import { + IndentArticleContainer, + NoteHeaderDate, + NoteMarkdown, + NoteMarkdownImageRecordProvider, + NoteTitle, +} from '~/app/notes/[id]/pageExtra' +import { ClientOnly } from '~/components/common/ClientOnly' +import { Paper } from '~/components/layout/container/Paper' +import { Loading } from '~/components/ui/loading' +import { + CurrentNoteDataAtomProvider, + CurrentNoteDataProvider, +} from '~/providers/note/CurrentNoteDataProvider' +import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider' +import { queries } from '~/queries/definition' + +import { NoteBanner, NoteHideIfSecret, NoteMetaBar } from '../note' +import { BanCopyWrapper } from '../shared/BanCopyWrapper' +import { XLogSummaryForNote } from '../xlog' + +interface NotePreviewProps { + noteId: number +} +export const NotePreview: FC = (props) => { + const { data, isLoading } = useQuery({ + ...queries.note.byNid(props.noteId.toString()), + }) + const overrideAtom = useMemo( + () => atom(null as null | NoteWrappedPayload), + [data], + ) + if (isLoading) return + if (!data) return null + return ( + + + + +
    + + + + + + + + +
    + +
    +
    + + + + + + + + + + + +
    +
    +
    + ) +} diff --git a/src/components/widgets/peek/PeekModal.tsx b/src/components/widgets/peek/PeekModal.tsx new file mode 100644 index 0000000000..288f1c69b4 --- /dev/null +++ b/src/components/widgets/peek/PeekModal.tsx @@ -0,0 +1,34 @@ +import { m } from 'framer-motion' +import Link from 'next/link' +import type { PropsWithChildren } from 'react' + +import { microReboundPreset } from '~/constants/spring' +import { useModalStack } from '~/providers/root/modal-stack-provider' + +export const PeekModal = ( + props: PropsWithChildren<{ + to: string + }>, +) => { + const { dismissTop } = useModalStack() + return ( + + {props.children} + + + + Go to this link + + + ) +} diff --git a/src/components/widgets/peek/PostPreview.tsx b/src/components/widgets/peek/PostPreview.tsx new file mode 100644 index 0000000000..99498aff76 --- /dev/null +++ b/src/components/widgets/peek/PostPreview.tsx @@ -0,0 +1,62 @@ +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import Balancer from 'react-wrap-balancer' +import { atom } from 'jotai' +import type { PostModel } from '@mx-space/api-client' +import type { FC } from 'react' + +import { + PostMarkdown, + PostMarkdownImageRecordProvider, + PostMetaBarInternal, +} from '~/app/posts/(post-detail)/[category]/[slug]/pageExtra' +import { Paper } from '~/components/layout/container/Paper' +import { Loading } from '~/components/ui/loading' +import { + CurrentPostDataAtomProvider, + CurrentPostDataProvider, +} from '~/providers/post/CurrentPostDataProvider' +import { WrappedElementProvider } from '~/providers/shared/WrappedElementProvider' +import { queries } from '~/queries/definition' + +import { PostOutdate } from '../post' +import { XLogSummaryForPost } from '../xlog' + +interface PostPreviewProps { + category: string + slug: string +} +export const PostPreview: FC = (props) => { + const { category, slug } = props + const { data, isLoading } = useQuery({ + ...queries.post.bySlug(category, slug), + }) + const overrideAtom = useMemo(() => atom(null as null | PostModel), [data]) + if (isLoading) return + if (!data) return null + return ( + + + +
    +
    +

    + {data.title} +

    + + + + + + +
    + + + + + +
    +
    +
    + ) +} diff --git a/src/components/widgets/post/PostActionAside.tsx b/src/components/widgets/post/PostActionAside.tsx index 117a7eab89..8c545f56ae 100644 --- a/src/components/widgets/post/PostActionAside.tsx +++ b/src/components/widgets/post/PostActionAside.tsx @@ -12,8 +12,8 @@ import { routeBuilder, Routes } from '~/lib/route-builder' import { toast } from '~/lib/toast' import { urlBuilder } from '~/lib/url-builder' import { - getCurrentPostData, - setCurrentPostData, + getGlobalCurrentPostData, + setGlobalCurrentPostData, useCurrentPostDataSelector, } from '~/providers/post/CurrentPostDataProvider' import { useModalStack } from '~/providers/root/modal-stack-provider' @@ -44,7 +44,7 @@ const LikeButton = () => { apiClient.post.thumbsUp(id).then(() => { setLikeId(id) - setCurrentPostData((draft) => { + setGlobalCurrentPostData((draft) => { draft.count.like += 1 }) update() @@ -110,7 +110,7 @@ const ShareButton = () => { aria-label="Share This Post Button" className="flex flex-col space-y-2" onClick={() => { - const post = getCurrentPostData() + const post = getGlobalCurrentPostData() if (!post) return diff --git a/src/providers/internal/createDataProvider.tsx b/src/providers/internal/createDataProvider.tsx index 7e41e7372a..fd6faa5d12 100644 --- a/src/providers/internal/createDataProvider.tsx +++ b/src/providers/internal/createDataProvider.tsx @@ -1,9 +1,10 @@ 'use client' -import { memo, useCallback, useEffect } from 'react' +import { createContext, memo, useCallback, useContext, useEffect } from 'react' import { produce } from 'immer' -import { atom, useAtomValue } from 'jotai' +import { atom, useAtomValue, useSetAtom } from 'jotai' import { selectAtom } from 'jotai/utils' +import type { PrimitiveAtom } from 'jotai' import type { FC, PropsWithChildren } from 'react' import { useBeforeMounted } from '~/hooks/common/use-before-mounted' @@ -11,12 +12,31 @@ import { noopArr } from '~/lib/noop' import { jotaiStore } from '~/lib/store' export const createDataProvider = () => { - const currentDataAtom = atom(null) + const CurrentDataAtomContext = createContext( + null! as PrimitiveAtom, + ) + const globalCurrentDataAtom = atom(null) + const CurrentDataAtomProvider: FC< + PropsWithChildren<{ + overrideAtom?: PrimitiveAtom + }> + > = ({ children, overrideAtom }) => { + return ( + + {children} + + ) + } const CurrentDataProvider: FC< { data: Model } & PropsWithChildren > = memo(({ data, children }) => { + const currentDataAtom = + useContext(CurrentDataAtomContext) ?? globalCurrentDataAtom + useBeforeMounted(() => { jotaiStore.set(currentDataAtom, data) }) @@ -35,10 +55,13 @@ export const createDataProvider = () => { }) CurrentDataProvider.displayName = 'CurrentDataProvider' + const useCurrentDataSelector = ( selector: (data: Model | null) => T, deps?: any[], ) => { + const currentDataAtom = + useContext(CurrentDataAtomContext) ?? globalCurrentDataAtom const nextSelector = useCallback((data: Model | null) => { return data ? selector(data) : null }, deps || noopArr) @@ -46,21 +69,26 @@ export const createDataProvider = () => { return useAtomValue(selectAtom(currentDataAtom, nextSelector)) } - const setCurrentData = (recipe: (draft: Model) => void) => { + const useSetCurrentData = () => + useSetAtom(useContext(CurrentDataAtomContext) ?? globalCurrentDataAtom) + + const setGlobalCurrentData = (recipe: (draft: Model) => void) => { jotaiStore.set( - currentDataAtom, - produce(jotaiStore.get(currentDataAtom), recipe), + globalCurrentDataAtom, + produce(jotaiStore.get(globalCurrentDataAtom), recipe), ) } - const getCurrentData = () => { - return jotaiStore.get(currentDataAtom) + const getGlobalCurrentData = () => { + return jotaiStore.get(globalCurrentDataAtom) } return { + CurrentDataAtomProvider, CurrentDataProvider, useCurrentDataSelector, - setCurrentData, - getCurrentData, + useSetCurrentData, + setGlobalCurrentData, + getGlobalCurrentData, } } diff --git a/src/providers/note/CurrentNoteDataProvider.tsx b/src/providers/note/CurrentNoteDataProvider.tsx index 1bbb912eda..e3a2e0d857 100644 --- a/src/providers/note/CurrentNoteDataProvider.tsx +++ b/src/providers/note/CurrentNoteDataProvider.tsx @@ -12,16 +12,20 @@ import { createDataProvider } from '../internal/createDataProvider' const { CurrentDataProvider, - getCurrentData, - setCurrentData, + CurrentDataAtomProvider, + getGlobalCurrentData: getCurrentData, + setGlobalCurrentData: setCurrentData, useCurrentDataSelector, + useSetCurrentData, } = createDataProvider() export { CurrentDataProvider as CurrentNoteDataProvider, + CurrentDataAtomProvider as CurrentNoteDataAtomProvider, getCurrentData as getCurrentNoteData, setCurrentData as setCurrentNoteData, useCurrentDataSelector as useCurrentNoteDataSelector, + useSetCurrentData as useSetCurrentNoteData, } export const SyncNoteDataAfterLoggedIn = () => { diff --git a/src/providers/note/CurrentNoteIdProvider.tsx b/src/providers/note/CurrentNoteIdProvider.tsx index 4decef8150..a648ae5d85 100644 --- a/src/providers/note/CurrentNoteIdProvider.tsx +++ b/src/providers/note/CurrentNoteIdProvider.tsx @@ -25,6 +25,7 @@ const CurrentNoteIdProvider: FC< return children }) +CurrentNoteIdProvider.displayName = 'CurrentNoteIdProvider' const useCurrentNoteId = () => { return useAtomValue(currentNoteIdAtom) } diff --git a/src/providers/page/CurrentPageDataProvider.tsx b/src/providers/page/CurrentPageDataProvider.tsx index 3c70aef531..d2e9157922 100644 --- a/src/providers/page/CurrentPageDataProvider.tsx +++ b/src/providers/page/CurrentPageDataProvider.tsx @@ -8,8 +8,8 @@ import { createDataProvider } from '../internal/createDataProvider' const { CurrentDataProvider, - getCurrentData, - setCurrentData, + getGlobalCurrentData: getCurrentData, + setGlobalCurrentData: setCurrentData, useCurrentDataSelector, } = createDataProvider() diff --git a/src/providers/post/CurrentPostDataProvider.tsx b/src/providers/post/CurrentPostDataProvider.tsx index f87455a834..0fac007a5d 100644 --- a/src/providers/post/CurrentPostDataProvider.tsx +++ b/src/providers/post/CurrentPostDataProvider.tsx @@ -8,21 +8,23 @@ import { createDataProvider } from '../internal/createDataProvider' const { CurrentDataProvider, - getCurrentData, - setCurrentData, + CurrentDataAtomProvider, + getGlobalCurrentData, + setGlobalCurrentData, useCurrentDataSelector, } = createDataProvider() declare global { interface Window { - getCurrentPostData: typeof getCurrentData + getCurrentPostData: typeof getGlobalCurrentData } } -if (isDev && isClientSide) window.getCurrentPostData = getCurrentData +if (isDev && isClientSide) window.getCurrentPostData = getGlobalCurrentData export { CurrentDataProvider as CurrentPostDataProvider, - getCurrentData as getCurrentPostData, - setCurrentData as setCurrentPostData, + CurrentDataAtomProvider as CurrentPostDataAtomProvider, + getGlobalCurrentData as getGlobalCurrentPostData, + setGlobalCurrentData as setGlobalCurrentPostData, useCurrentDataSelector as useCurrentPostDataSelector, } diff --git a/src/providers/root/modal-stack-provider.tsx b/src/providers/root/modal-stack-provider.tsx index 4e8b9297a4..d8a62145d1 100644 --- a/src/providers/root/modal-stack-provider.tsx +++ b/src/providers/root/modal-stack-provider.tsx @@ -11,7 +11,7 @@ import { import { AnimatePresence, m, useAnimationControls } from 'framer-motion' import { atom, useAtomValue, useSetAtom } from 'jotai' import type { Target, Transition } from 'framer-motion' -import type { FC, PropsWithChildren } from 'react' +import type { FC, PropsWithChildren, SyntheticEvent } from 'react' import { CloseIcon } from '~/components/icons/close' import { Divider } from '~/components/ui/divider' @@ -26,8 +26,10 @@ const modalIdToPropsMap = {} as Record interface ModalProps { title: string content: FC<{}> - + CustomModalComponent?: FC + clickOutsideToDismiss?: boolean modalClassName?: string + modalContainerClassName?: string } const modalStackAtom = atom([] as (ModalProps & { id: string })[]) @@ -131,30 +133,74 @@ export const Modal: Component<{ useEffect(() => { animateController.start(enterStyle) }, []) + const { + CustomModalComponent, + modalClassName, + content, + title, + clickOutsideToDismiss, + modalContainerClassName, + } = item + const modalStyle = useMemo(() => ({ zIndex: 99 + index }), [index]) + const dismiss = useCallback( + (e: SyntheticEvent) => { + stopPropagation(e) + close() + }, + [close], + ) + const noticeModal = useCallback(() => { + animateController + .start({ + scale: 1.05, + transition: { + duration: 0.06, + }, + }) + .then(() => { + animateController.start({ + scale: 1, + }) + }) + }, [animateController]) + if (CustomModalComponent) { + return ( + + + + +
    +
    + + {createElement(content)} + +
    +
    +
    +
    +
    + ) + } return (
    { - animateController - .start({ - scale: 1.05, - transition: { - duration: 0.06, - }, - }) - .then(() => { - animateController.start({ - scale: 1, - }) - }) - }} + className={clsxm( + 'fixed inset-0 z-[20] flex center', + modalContainerClassName, + )} + onClick={clickOutsideToDismiss ? dismiss : noticeModal} > ({ zIndex: 99 + index }), [index])} + style={modalStyle} exit={initialStyle} initial={initialStyle} animate={animateController} @@ -165,17 +211,17 @@ export const Modal: Component<{ 'p-2 shadow-2xl shadow-stone-300 backdrop-blur-sm dark:shadow-stone-800', 'max-h-[70vh] min-w-[300px] max-w-[90vw] lg:max-h-[calc(100vh-20rem)] lg:max-w-[50vw]', 'border border-slate-200 dark:border-neutral-800', - item.modalClassName, + modalClassName, )} onClick={stopPropagation} > - {item.title} + {title}
    - {createElement(item.content)} + {createElement(content)}
    { + if (getGlobalCurrentPostData()?.id === post.id) { + setGlobalCurrentPostData((draft) => { const nextPost = { ...data } Reflect.deleteProperty(nextPost, 'category') Object.assign(draft, nextPost) @@ -56,7 +56,7 @@ export const eventHandler = ( case EventTypes.POST_DELETE: { const post = data as PostModel - if (getCurrentPostData()?.id === post.id) { + if (getGlobalCurrentPostData()?.id === post.id) { router.replace(routeBuilder(Routes.PageDeletd, {})) toast.error('文章已删除') } diff --git a/tailwind.config.ts b/tailwind.config.ts index 2a0874e9eb..83b436ff4b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -298,7 +298,7 @@ function addShortcutPlugin({ addUtilities }: PluginAPI) { 'justify-content': 'center', }, '.fill-content': { - 'min-height': `calc(100vh - 4.5rem - 120px)`, + 'min-height': `calc(100vh - 17.5rem)`, }, } addUtilities(styles)