diff --git a/global.d.ts b/global.d.ts index fdcf8203cc..fc132ae15d 100644 --- a/global.d.ts +++ b/global.d.ts @@ -37,3 +37,24 @@ declare global { } export {} + +declare module '@mx-space/api-client' { + export interface PostMeta { + style?: string + cover?: string + banner?: string | { type: string; message: string } + } + interface TextBaseModel extends BaseCommentIndexModel { + meta?: PostMeta + } + + interface AggregateTopNote { + meta?: PostMeta + } + + interface AggregateTopPost { + meta?: PostMeta + } +} + +export {} diff --git a/package.json b/package.json index 9f4d50a125..7312f2d550 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "markdown-escape": "2.0.0", "markdown-to-jsx": "npm:@innei/markdown-to-jsx@7.1.3-beta.2", "mdast-util-toc": "6.1.1", + "medium-zoom": "1.0.8", "next": "13.4.6", "next-themes": "0.2.1", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05467e7557..31053eee1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: mdast-util-toc: specifier: 6.1.1 version: 6.1.1 + medium-zoom: + specifier: 1.0.8 + version: 1.0.8 next: specifier: 13.4.6 version: 13.4.6(@babel/core@7.21.0)(react-dom@18.2.0)(react@18.2.0) @@ -5172,6 +5175,10 @@ packages: unist-util-visit: 4.1.2 dev: false + /medium-zoom@1.0.8: + resolution: {integrity: sha512-CjFVuFq/IfrdqesAXfg+hzlDKu6A2n80ZIq0Kl9kWjoHh9j1N9Uvk5X0/MmN0hOfm5F9YBswlClhcwnmtwz7gA==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} diff --git a/src/app/error.tsx b/src/app/error.tsx index f153ee8a79..44f568688a 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,11 +1,5 @@ 'use client' export default ({ error, reset }: any) => { - return ( - - -
Something went wrong
- - - ) + return
Something went wrong
} diff --git a/src/app/notes/[id]/page.tsx b/src/app/notes/[id]/page.tsx index 2f64d74535..17302c7c95 100644 --- a/src/app/notes/[id]/page.tsx +++ b/src/app/notes/[id]/page.tsx @@ -17,7 +17,9 @@ import { DividerVertical } from '~/components/ui/divider' import { FloatPopover } from '~/components/ui/float-popover' import { Loading } from '~/components/ui/loading' import { Markdown } from '~/components/ui/markdown' +import { NoteTopic } from '~/components/widgets/note/NoteTopic' import { Toc, TocAutoScroll } from '~/components/widgets/toc' +import { XLogInfoForNote, XLogSummaryForNote } from '~/components/widgets/xlog' import { useBeforeMounted } from '~/hooks/common/use-before-mounted' import { useNoteByNidQuery, useNoteData } from '~/hooks/data/use-note' import { mood2icon, weather2icon } from '~/lib/meta-icon' @@ -67,31 +69,41 @@ const PageImpl = () => { }` return ( -
-
- - - {tips} - - - - - -
- - - - - - - - - - - -
+ <> +
+
+ + + {tips} + + + + + +
+ + + + + + + + + + + + + +
+ {!!note.topic && } + + ) } @@ -111,7 +123,7 @@ const NoteMetaBar = () => { if (!note) return null const children = [] as ReactNode[] - if (note.weather || !note.mood) { + if (note.weather || note.mood) { children.push() } @@ -144,6 +156,16 @@ const NoteMetaBar = () => { ) } + if (note.count.like > 0) { + children.push( + , + + + {note.count.like} + , + ) + } + return children } @@ -165,7 +187,7 @@ const NoteDateMeta = () => { ) } -const Markdownrenderers: { [name: string]: Partial } = { +const MarkdownRenderers: { [name: string]: Partial } = { text: { react(node, _, state) { return ( diff --git a/src/app/notes/layout.tsx b/src/app/notes/layout.tsx index 69ab4d5a7e..d852bdbb93 100644 --- a/src/app/notes/layout.tsx +++ b/src/app/notes/layout.tsx @@ -12,7 +12,7 @@ export default async (props: PropsWithChildren) => { className={clsx( 'relative mx-auto grid min-h-screen max-w-[60rem]', 'gap-4 md:grid-cols-1 lg:max-w-[calc(60rem+400px)] lg:grid-cols-[1fr_minmax(auto,60rem)_1fr]', - 'mt-24', + 'mt-12 md:mt-24', )} > diff --git a/src/components/common/AutoResizeHeight.tsx b/src/components/common/AutoResizeHeight.tsx new file mode 100644 index 0000000000..6c7d5f811a --- /dev/null +++ b/src/components/common/AutoResizeHeight.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useRef, useState } from 'react' +import { motion } from 'framer-motion' + +import { clsxm } from '~/utils/helper' + +interface AnimateChangeInHeightProps { + children: React.ReactNode + className?: string + duration?: number +} + +export const AutoResizeHeight: React.FC = ({ + children, + className, + duration = 0.6, +}) => { + const containerRef = useRef(null) + const [height, setHeight] = useState('auto') + + useEffect(() => { + if (containerRef.current) { + const resizeObserver = new ResizeObserver((entries) => { + // We only have one entry, so we can use entries[0]. + const observedHeight = entries[0].contentRect.height + setHeight(observedHeight) + }) + + resizeObserver.observe(containerRef.current) + + return () => { + // Cleanup the observer when the component is unmounted + resizeObserver.disconnect() + } + } + }, []) + + return ( + +
{children}
+
+ ) +} diff --git a/src/components/common/Lazyload.tsx b/src/components/common/Lazyload.tsx index b82e090696..aaf95e3504 100644 --- a/src/components/common/Lazyload.tsx +++ b/src/components/common/Lazyload.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { useInView } from 'react-intersection-observer' import type { FC, PropsWithChildren } from 'react' import type { IntersectionOptions } from 'react-intersection-observer' @@ -14,9 +14,16 @@ export const LazyLoad: FC = (props) => { rootMargin: `${offset || 0}px`, ...rest, }) + const [isLoaded, setIsLoaded] = React.useState(false) + useEffect(() => { + if (inView) { + setIsLoaded(true) + } + }, [inView]) + return ( <> - + {!isLoaded && } {!inView ? placeholder : props.children} ) diff --git a/src/components/layout/header/internal/AnimatedLogo.tsx b/src/components/layout/header/internal/AnimatedLogo.tsx index ec01b63471..eabaa52734 100644 --- a/src/components/layout/header/internal/AnimatedLogo.tsx +++ b/src/components/layout/header/internal/AnimatedLogo.tsx @@ -21,6 +21,7 @@ export const AnimatedLogo = () => { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} + className="scale-75" > diff --git a/src/components/layout/header/internal/HeaderActionButton.tsx b/src/components/layout/header/internal/HeaderActionButton.tsx index a44ab4cbf8..ae2be19e0c 100644 --- a/src/components/layout/header/internal/HeaderActionButton.tsx +++ b/src/components/layout/header/internal/HeaderActionButton.tsx @@ -1,13 +1,17 @@ -export const HeaderActionButton: Component = ({ - children, - ...rest -}) => { +import { forwardRef } from 'react' +import type { ForwardRefComponent } from 'framer-motion' + +export const HeaderActionButton: ForwardRefComponent< + HTMLButtonElement, + JSX.IntrinsicElements['button'] +> = forwardRef(({ children, ...rest }, ref) => { return ( ) -} +}) diff --git a/src/components/layout/header/internal/HeaderDrawerButton.tsx b/src/components/layout/header/internal/HeaderDrawerButton.tsx index d01b8d2922..36cd631084 100644 --- a/src/components/layout/header/internal/HeaderDrawerButton.tsx +++ b/src/components/layout/header/internal/HeaderDrawerButton.tsx @@ -8,8 +8,9 @@ import Link from 'next/link' import type { SVGProps } from 'react' import { CloseIcon } from '~/components/icons/close' -import { MotionButtonBase } from '~/components/ui/button/MotionButton' +import { MotionButtonBase } from '~/components/ui/button' import { reboundPreset } from '~/constants/spring' +import { useIsClient } from '~/hooks/common/use-is-client' import { jotaiStore } from '~/lib/store' import { HeaderActionButton } from './HeaderActionButton' @@ -30,50 +31,56 @@ const drawerOpenAtom = atom(false) export const HeaderDrawerButton = () => { const [open, setOpen] = useAtom(drawerOpenAtom) + const isClient = useIsClient() + const ButtonElement = ( + + + + ) + if (!isClient) return ButtonElement + return ( setOpen(open)}> - - - - - + {ButtonElement} - - {open && ( - <> - - - +
+ + {open && ( + <> + + + - - - - { - setOpen(false) - }} - > - - - + + + + { + setOpen(false) + }} + > + + + - - - - - )} - + + + + + )} + +
) diff --git a/src/components/ui/avatar/Avatar.tsx b/src/components/ui/avatar/Avatar.tsx new file mode 100644 index 0000000000..679d0ae28e --- /dev/null +++ b/src/components/ui/avatar/Avatar.tsx @@ -0,0 +1,83 @@ +import React, { createElement, useRef, useState } from 'react' +import type { DetailedHTMLProps, FC, ImgHTMLAttributes } from 'react' + +import { clsxm } from '~/utils/helper' + +import { FlexText } from '../text' + +interface AvatarProps { + url?: string + imageUrl?: string + size?: number + + wrapperProps?: JSX.IntrinsicElements['div'] + + shadow?: boolean + text?: string + + lazy?: boolean +} + +export const Avatar: FC< + AvatarProps & + DetailedHTMLProps, HTMLImageElement> +> = (props) => { + const { shadow = true, lazy = true } = props + const avatarRef = useRef(null) + + const [loaded, setLoaded] = useState(!lazy) + + const { wrapperProps = {} } = props + const { className, ...restProps } = wrapperProps + + return ( +
+ {createElement( + props.url ? 'a' : 'div', + { + className: 'relative inline-block h-full w-full', + + ...(props.url + ? { + href: props.url, + target: '_blank', + rel: 'noreferrer', + } + : {}), + }, + props.imageUrl ? ( +
+ setLoaded(true)} + loading={lazy ? 'lazy' : 'eager'} + className="aspect-square" + /> +
+ ) : props.text ? ( +
+ +
+ ) : null, + )} +
+ ) +} diff --git a/src/components/ui/avatar/index.ts b/src/components/ui/avatar/index.ts new file mode 100644 index 0000000000..227ecdba36 --- /dev/null +++ b/src/components/ui/avatar/index.ts @@ -0,0 +1 @@ +export * from './Avatar' diff --git a/src/components/ui/button/MotionButton.tsx b/src/components/ui/button/MotionButton.tsx index 430ccad6d5..1541212f80 100644 --- a/src/components/ui/button/MotionButton.tsx +++ b/src/components/ui/button/MotionButton.tsx @@ -1,11 +1,14 @@ -import { memo } from 'react' +import { forwardRef, memo } from 'react' import { motion } from 'framer-motion' -import type { HTMLMotionProps } from 'framer-motion' +import type { ForwardRefComponent, HTMLMotionProps } from 'framer-motion' import { microReboundPreset } from '~/constants/spring' -export const MotionButtonBase: Component> = memo( - ({ children, ...rest }) => { +export const MotionButtonBase: ForwardRefComponent< + HTMLButtonElement, + HTMLMotionProps<'button'> +> = memo( + forwardRef(({ children, ...rest }, ref) => { return ( > = memo( whileTap={{ scale: 0.95 }} transition={{ ...microReboundPreset }} {...rest} + ref={ref} > {children} ) - }, + }), ) diff --git a/src/components/ui/button/index.ts b/src/components/ui/button/index.ts new file mode 100644 index 0000000000..217894b891 --- /dev/null +++ b/src/components/ui/button/index.ts @@ -0,0 +1 @@ +export * from './MotionButton' diff --git a/src/components/ui/image/LazyImage.tsx b/src/components/ui/image/LazyImage.tsx deleted file mode 100644 index 2b8c49469e..0000000000 --- a/src/components/ui/image/LazyImage.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import React, { - forwardRef, - memo, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, -} from 'react' -import { clsx } from 'clsx' -import mediumZoom from 'medium-zoom' -import type { - CSSProperties, - DetailedHTMLProps, - FC, - ImgHTMLAttributes, -} from 'react' - -import styles from './index.module.css' -import { useCalculateNaturalSize } from './use-calculate-size' - -interface ImageProps { - defaultImage?: string - src: string - alt?: string - height?: number | string - width?: number | string - backgroundColor?: string - popup?: boolean - overflowHidden?: boolean - getParentElWidth?: ((parentElementWidth: number) => number) | number - showErrorMessage?: boolean -} - -const Image: FC< - { - popup?: boolean - height?: number | string - width?: number | string - loaderFn: () => void - loaded: boolean - } & Pick< - DetailedHTMLProps, HTMLImageElement>, - 'src' | 'alt' - > -> = memo(({ src, alt, height, width, popup = false, loaded, loaderFn }) => { - const imageRef = useRef(null) - - useEffect(() => { - if (!popup) { - return - } - const $image = imageRef.current - if ($image) { - const zoom = mediumZoom($image, { - background: 'var(--light-bg)', - }) - - return () => { - zoom.detach(zoom.getImages()) - } - } - }, [popup]) - - useEffect(() => { - loaderFn() - }, [loaderFn]) - - return ( - <> -
- {alt} -
- - ) -}) - -const onImageAnimationEnd: React.AnimationEventHandler = ( - e, -) => { - ;(e.target as HTMLElement).dataset.animated = '1' -} - -export type ImageLazyRef = { status: 'loading' | 'loaded' } - -export const LazyImage = memo( - forwardRef< - ImageLazyRef, - ImageProps & - DetailedHTMLProps, HTMLImageElement> - >((props, ref) => { - const { - defaultImage, - src, - alt, - height, - width, - backgroundColor = 'rgb(111,111,111)', - popup = false, - style, - overflowHidden = false, - getParentElWidth = (w) => w, - showErrorMessage, - ...rest - } = props - useImperativeHandle(ref, () => { - return { - status: loaded ? 'loaded' : ('loading' as any), - } - }) - const realImageRef = useRef(null) - const placeholderRef = useRef(null) - - const wrapRef = useRef(null) - const [calculatedSize, calculateDimensions] = useCalculateNaturalSize() - - const [loaded, setLoad] = useState(false) - const loaderFn = useCallback(() => { - if (!src || loaded) { - return - } - - const image = new window.Image() - image.src = src as string - // FIXME - const parentElement = wrapRef.current?.parentElement?.parentElement - - if (!height && !width) { - calculateDimensions( - image, - typeof getParentElWidth == 'function' - ? getParentElWidth( - parentElement - ? parseFloat(getComputedStyle(parentElement).width) - : 0, - ) - : getParentElWidth, - ) - } - - image.onload = () => { - setLoad(true) - try { - if (placeholderRef && placeholderRef.current) { - placeholderRef.current.classList.add('hide') - } - - // eslint-disable-next-line no-empty - } catch {} - } - if (showErrorMessage) { - image.onerror = () => { - try { - if (placeholderRef && placeholderRef.current) { - placeholderRef.current.innerHTML = `

图片加载失败!
- ${escapeHTMLTag(image.src)}

` - } - // eslint-disable-next-line no-empty - } catch {} - } - } - }, [ - src, - loaded, - height, - width, - calculateDimensions, - getParentElWidth, - backgroundColor, - showErrorMessage, - ]) - const memoPlaceholderImage = useMemo( - () => ( - - ), - [backgroundColor, height, width], - ) - - const imageWrapperStyle = useMemo( - () => ({ - height: loaded ? undefined : height || calculatedSize.height, - width: loaded ? undefined : width || calculatedSize.width, - - ...(overflowHidden ? { overflow: 'hidden', borderRadius: '3px' } : {}), - }), - [ - calculatedSize.height, - calculatedSize.width, - height, - loaded, - overflowHidden, - width, - ], - ) - return ( -
- {defaultImage ? ( - {alt} - ) : ( -
- - {alt} - {!loaded && memoPlaceholderImage} - -
- )} - {alt &&
{alt}
} -
- ) - }), -) - -const PlaceholderImage = memo( - forwardRef< - HTMLDivElement, - { ref: any; className?: string } & Partial - >((props, ref) => { - const { backgroundColor, height, width } = props - return ( -
- ) - }), -) diff --git a/src/components/ui/image/ZoomedImage.tsx b/src/components/ui/image/ZoomedImage.tsx index 871319ee49..56a09c764b 100644 --- a/src/components/ui/image/ZoomedImage.tsx +++ b/src/components/ui/image/ZoomedImage.tsx @@ -1,13 +1,8 @@ 'use client' -import { useCallback, useId, useMemo, useRef, useState } from 'react' -import { - AnimatePresence, - motion, - useAnimationControls, - useDomEvent, -} from 'framer-motion' -import { useTheme } from 'next-themes' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { motion, useAnimationControls } from 'framer-motion' +import mediumZoom from 'medium-zoom' import { tv } from 'tailwind-variants' import type { FC, ReactNode } from 'react' @@ -19,7 +14,6 @@ import { useMarkdownImageRecord } from '~/providers/article/markdown-image-recor import { clsxm } from '~/utils/helper' import { Divider } from '../divider' -import { RootPortal } from '../portal' type TImageProps = { src: string @@ -73,12 +67,28 @@ export const ImageLazy: Component = ({ ) const controls = useAnimationControls() - const currentId = useId() - const [imageZooming, setImageZooming] = useState(false) + + const imageRef = useRef(null) + useEffect(() => { + if (!zoom) { + return + } + const $image = imageRef.current + + if ($image) { + const zoom = mediumZoom($image, { + background: 'var(--sbg)', + }) + + return () => { + zoom.detach(zoom.getImages()) + } + } + }, [zoom]) return ( -
+
{imageLoadStatus !== ImageLoadStatus.Loaded && placeholder} @@ -94,15 +104,12 @@ export const ImageLazy: Component = ({ // filter: 'blur(0px)', }, }} - layoutId={currentId} initial="loading" animate={controls} src={src} title={title} alt={alt} - onClick={() => { - zoom && setImageZooming(true) - }} + ref={imageRef} onLoad={() => { setImageLoadStatusSafe(ImageLoadStatus.Loaded) requestAnimationFrame(() => { @@ -116,15 +123,6 @@ export const ImageLazy: Component = ({ /> - { - setImageZooming(false) - }} - show={imageZooming} - src={src} - alt={alt} - /> {!!figcaption && (
@@ -136,58 +134,6 @@ export const ImageLazy: Component = ({ ) } -const ImagePreview = (props: { - src?: string - show: boolean - onClose: () => void - alt?: string - id: string -}) => { - const isDark = useTheme().theme === 'dark' - const { alt, show, onClose, src, id } = props - useDomEvent(useRef(window), 'scroll', () => show && onClose()) - - return ( - - - {show && ( - - )} - - {show && ( -
{ - onClose() - }} - > - -
- )} -
- ) -} - export const ZoomedImage: Component = (props) => { return ( diff --git a/src/components/ui/markdown/Markdown.tsx b/src/components/ui/markdown/Markdown.tsx index 5ff1fd70fe..8ab5f5f176 100644 --- a/src/components/ui/markdown/Markdown.tsx +++ b/src/components/ui/markdown/Markdown.tsx @@ -6,7 +6,7 @@ import type { MarkdownToJSX } from 'markdown-to-jsx' import type { FC, PropsWithChildren } from 'react' import { FixedZoomedImage } from '../image' -import styles from './index.module.css' +import styles from './markdown.module.css' import { CommentAtRule } from './parsers/comment-at' import { ContainerRule } from './parsers/container' import { InsertRule } from './parsers/ins' diff --git a/src/components/ui/markdown/index.module.css b/src/components/ui/markdown/markdown.module.css similarity index 94% rename from src/components/ui/markdown/index.module.css rename to src/components/ui/markdown/markdown.module.css index 676ef9dadf..bd9928af01 100644 --- a/src/components/ui/markdown/index.module.css +++ b/src/components/ui/markdown/markdown.module.css @@ -56,4 +56,8 @@ a { @apply border-b-[0.5px] border-current no-underline duration-200 hover:text-accent; } + + hr { + @apply mx-auto w-[60px]; + } } diff --git a/src/components/ui/markdown/renderers/paragraph.tsx b/src/components/ui/markdown/renderers/paragraph.tsx index 5c09ac860f..670ec31f3a 100644 --- a/src/components/ui/markdown/renderers/paragraph.tsx +++ b/src/components/ui/markdown/renderers/paragraph.tsx @@ -1,14 +1,23 @@ +import React from 'react' import clsx from 'clsx' import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react' -import React from 'react' export const MParagraph: FC< DetailedHTMLProps, HTMLParagraphElement> > = (props) => { const { children, ...other } = props const { className, ...rest } = other + + if (React.Children.count(children) === 1) { + const child = React.Children.toArray(children)[0] + if (typeof child === 'object') { + if ((child as any)?.props?.src) { + return children + } + } + } return ( -

+

{children}

) diff --git a/src/components/ui/text/FlexText.tsx b/src/components/ui/text/FlexText.tsx new file mode 100644 index 0000000000..222a88edd3 --- /dev/null +++ b/src/components/ui/text/FlexText.tsx @@ -0,0 +1,37 @@ +import React, { memo, useEffect, useRef, useState } from 'react' +import type { FC } from 'react' + +// TODO: wait for new CSS unit +export const FlexText: FC<{ text: string; scale: number }> = memo((props) => { + const ref = useRef(null) + const [done, setDone] = useState(false) + + useEffect(() => { + if (!ref.current) { + return + } + + const $el = ref.current + const $parent = $el.parentElement + let observe: ResizeObserver + if ($parent) { + observe = new ResizeObserver(() => { + const { width } = $parent.getBoundingClientRect() + $el.style.fontSize = `${(width / props.text.length) * props.scale}px` + setDone(true) + }) + observe.observe($parent) + } + + return () => { + if (observe) { + observe.disconnect() + } + } + }, [props.scale]) + return ( + + {props.text} + + ) +}) diff --git a/src/components/ui/text/index.ts b/src/components/ui/text/index.ts new file mode 100644 index 0000000000..2882e4c230 --- /dev/null +++ b/src/components/ui/text/index.ts @@ -0,0 +1 @@ +export * from './FlexText' diff --git a/src/components/widgets/note/NoteFooterNavigation.tsx b/src/components/widgets/note/NoteFooterNavigation.tsx new file mode 100644 index 0000000000..8b92af032e --- /dev/null +++ b/src/components/widgets/note/NoteFooterNavigation.tsx @@ -0,0 +1,88 @@ +'use client' + +import { memo } from 'react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import type { FC } from 'react' + +import { + IcRoundKeyboardDoubleArrowLeft, + IcRoundKeyboardDoubleArrowRight, +} from '~/components/icons/arrow' +import { MdiClockTimeThreeOutline } from '~/components/icons/clock' +import { Divider } from '~/components/ui/divider' +import { OnlyMobile } from '~/components/ui/viewport/OnlyMobile' +import { useNoteByNidQuery } from '~/hooks/data/use-note' +import { springScrollToTop } from '~/utils/scroller' + +export const NoteFooterNavigation: FC<{ id: string }> = memo(({ id }) => { + const { data } = useNoteByNidQuery(id) + + const router = useRouter() + + if (!data) return + + const { prev, next } = data + const prevNid = prev?.nid + const nextNid = next?.nid + + return ( + <> + {/* // 没有 0 的情况 */} + {(!!prevNid || !!nextNid) && ( + <> + +
+
+ {!!nextNid && ( + + + 前一篇 + + )} + + {!!prevNid && ( + + 后一篇 + + + )} +
+
{ + springScrollToTop() + router.push(`/timeline?type=note&id=${id}`) + }} + > + 时间线 + +
+
+ + )} + + ) +}) + +export const NoteFooterNavigationBarForMobile: typeof NoteFooterNavigation = ( + props, +) => { + return ( + + + + ) +} diff --git a/src/components/widgets/note/NoteTimeline.tsx b/src/components/widgets/note/NoteTimeline.tsx index 47fdb05c14..923549e4c4 100644 --- a/src/components/widgets/note/NoteTimeline.tsx +++ b/src/components/widgets/note/NoteTimeline.tsx @@ -1,8 +1,8 @@ 'use client' -import { useAutoAnimate } from '@formkit/auto-animate/react' import { useQuery } from '@tanstack/react-query' import { memo } from 'react' +import { AnimatePresence, motion } from 'framer-motion' import Link from 'next/link' import { tv } from 'tailwind-variants' @@ -14,7 +14,7 @@ import { apiClient } from '~/utils/request' export const NoteTimeline = () => { const note = useNoteData() const noteId = note?.id - const [animationParent] = useAutoAnimate() + // const [parent, enableAnimations] = useAutoAnimate() const { data: timelineData } = useQuery( ['notetimeline', noteId], async ({ queryKey }) => { @@ -42,19 +42,21 @@ export const NoteTimeline = () => { : [] return ( -
    - {(timelineData || initialData)?.map((item) => { - const isCurrent = item.id === noteId - return ( - - ) - })} -
+ +
    + {(timelineData || initialData)?.map((item) => { + const isCurrent = item.id === noteId + return ( + + ) + })} +
+
) } @@ -75,7 +77,11 @@ const MemoedItem = memo<{ const { active, nid, title } = props return ( -
  • + {title} -
  • + ) }) diff --git a/src/components/widgets/note/NoteTopic.tsx b/src/components/widgets/note/NoteTopic.tsx new file mode 100644 index 0000000000..57f7f2bea3 --- /dev/null +++ b/src/components/widgets/note/NoteTopic.tsx @@ -0,0 +1,60 @@ +import Link from 'next/link' +import type { TopicModel } from '@mx-space/api-client' +import type { FC } from 'react' + +import { Avatar } from '~/components/ui/avatar' +import { Divider } from '~/components/ui/divider' +import { FloatPopover } from '~/components/ui/float-popover' + +import { NoteTopicDetail } from './NoteTopicDetail' +import { NoteTopicMarkdownRender } from './NoteTopicMarkdownRender' + +const textToBigCharOrWord = (name: string | undefined) => { + if (!name) { + return '' + } + const splitOnce = name.split(' ')[0] + const bigChar = splitOnce.length > 4 ? name[0] : splitOnce + return bigChar +} + +export const NoteTopic: FC<{ topic: TopicModel }> = (props) => { + const { topic } = props + const { icon, name, introduce } = topic + + return ( +
    +
    + 文章被专栏收录: +
    + +
    + +
    + + ( + + {name} + + )} + > + + + + +
    + {introduce} +
    +
    +
    +
    + ) +} diff --git a/src/components/widgets/note/NoteTopicDetail.tsx b/src/components/widgets/note/NoteTopicDetail.tsx new file mode 100644 index 0000000000..934d8e65ed --- /dev/null +++ b/src/components/widgets/note/NoteTopicDetail.tsx @@ -0,0 +1,112 @@ +'use client' + +import { useQuery } from '@tanstack/react-query' +import Link from 'next/link' +import type { TopicModel } from '@mx-space/api-client' +import type { FC } from 'react' + +import { MdiClockOutline } from '~/components/icons/clock' +import { MdiFountainPenTip } from '~/components/icons/pen' +import { Divider, DividerVertical } from '~/components/ui/divider' +import { Loading } from '~/components/ui/loading' +import { RelativeTime } from '~/components/ui/relative-time' +import { useIsClient } from '~/hooks/common/use-is-client' +import { useNoteData } from '~/hooks/data/use-note' +import { apiClient } from '~/utils/request' + +import { NoteTopicMarkdownRender } from './NoteTopicMarkdownRender' + +export const NoteTopicDetail: FC<{ topic: TopicModel }> = (props) => { + const { topic } = props + const { id: topicId } = topic + + const { data, isLoading } = useQuery([`topic-${topicId}`], () => + apiClient.note.getNoteByTopicId(topicId, 1, 1, { + sortBy: 'created', + sortOrder: -1, + }), + ) + + const isClient = useIsClient() + if (!isClient) { + return null + } + + return ( +
    + +

    + {topic.name} +

    + + +
    + {topic.introduce} +
    + {topic.description && ( + <> + +
    + + {topic.description} + +
    + + )} + + + {isLoading ? ( + + ) : ( + data?.data[0] && ( +

    + + + 最近更新 + + + + {data?.data[0]?.title} + + + ( + + ) + + +

    + ) + )} + + {!isLoading && ( + <> + +

    + + + 共有文章: + {data?.pagination?.total} 篇 +

    + + )} +
    + ) +} + +export const ToTopicLink: FC = () => { + const note = useNoteData() + if (!note?.topic) return null + return ( + + + {note?.topic?.name} + + + ) +} diff --git a/src/components/widgets/note/NoteTopicInfo.tsx b/src/components/widgets/note/NoteTopicInfo.tsx index 36bc3ea783..27bc451fc0 100644 --- a/src/components/widgets/note/NoteTopicInfo.tsx +++ b/src/components/widgets/note/NoteTopicInfo.tsx @@ -1,21 +1,10 @@ 'use client' -import { useQuery } from '@tanstack/react-query' -import { memo } from 'react' -import Markdown from 'markdown-to-jsx' -import Link from 'next/link' -import type { TopicModel } from '@mx-space/api-client' -import type { MarkdownToJSX } from 'markdown-to-jsx' -import type { FC } from 'react' - -import { MdiClockOutline } from '~/components/icons/clock' -import { MdiFountainPenTip } from '~/components/icons/pen' -import { Divider, DividerVertical } from '~/components/ui/divider' +import { Divider } from '~/components/ui/divider' import { FloatPopover } from '~/components/ui/float-popover' -import { Loading } from '~/components/ui/loading' -import { RelativeTime } from '~/components/ui/relative-time' import { useNoteData } from '~/hooks/data/use-note' -import { apiClient } from '~/utils/request' + +import { NoteTopicDetail, ToTopicLink } from './NoteTopicDetail' export const NoteTopicInfo = () => { const note = useNoteData() @@ -35,121 +24,8 @@ export const NoteTopicInfo = () => { wrapperClassNames="flex flex-grow flex-shrink min-w-0" TriggerComponent={ToTopicLink} > - + ) } - -const InnerTopicDetail: FC<{ topic: TopicModel }> = (props) => { - const { topic } = props - const { id: topicId } = topic - - const { data, isLoading } = useQuery([`topic-${topicId}`], () => - apiClient.note.getNoteByTopicId(topicId, 1, 1, { - sortBy: 'created', - sortOrder: -1, - }), - ) - - return ( -
    - -

    - {topic.name} -

    - - -

    - {topic.introduce} -

    - {topic.description && ( - <> - -

    - - {topic.description} - -

    - - )} - - - {isLoading ? ( - - ) : ( - data?.data[0] && ( -

    - - - 最近更新 - - - - {data?.data[0]?.title} - - - ( - - ) - - -

    - ) - )} - - {!isLoading && ( - <> - -

    - - - 共有文章: - {data?.pagination?.total} 篇 -

    - - )} -
    - ) -} - -const mdOptions: MarkdownToJSX.Options = { - allowedTypes: [ - 'text', - 'paragraph', - 'codeInline', - 'link', - 'linkMailtoDetector', - 'linkBareUrlDetector', - 'linkAngleBraceStyleDetector', - 'textStrikethroughed', - 'textEmphasized', - 'textBolded', - 'textEscaped', - ], - forceBlock: true, - wrapper: ({ children }) =>
    {children}
    , -} -export const NoteTopicMarkdownRender: FC<{ children: string }> = memo( - (props) => { - return {props.children} - }, -) - -const ToTopicLink: FC = () => { - const note = useNoteData() - if (!note?.topic) return null - return ( - - - {note?.topic?.name} - - - ) -} diff --git a/src/components/widgets/note/NoteTopicMarkdownRender.tsx b/src/components/widgets/note/NoteTopicMarkdownRender.tsx new file mode 100644 index 0000000000..4b95b4eecb --- /dev/null +++ b/src/components/widgets/note/NoteTopicMarkdownRender.tsx @@ -0,0 +1,27 @@ +'use client' + +import Markdown from 'markdown-to-jsx' +import type { MarkdownToJSX } from 'markdown-to-jsx' +import type { FC } from 'react' + +const mdOptions: MarkdownToJSX.Options = { + allowedTypes: [ + 'text', + 'paragraph', + 'codeInline', + 'link', + 'linkMailtoDetector', + 'linkBareUrlDetector', + 'linkAngleBraceStyleDetector', + 'textStrikethroughed', + 'textEmphasized', + 'textBolded', + 'textEscaped', + ], + forceBlock: true, + wrapper: ({ children }) =>
    {children}
    , +} + +export const NoteTopicMarkdownRender: FC<{ children: string }> = (props) => { + return {props.children} +} diff --git a/src/components/widgets/xlog/XLogInfo.tsx b/src/components/widgets/xlog/XLogInfo.tsx new file mode 100644 index 0000000000..7f629d81a6 --- /dev/null +++ b/src/components/widgets/xlog/XLogInfo.tsx @@ -0,0 +1,155 @@ +import { useState } from 'react' +import { clsx } from 'clsx' +import type { FC, SVGProps } from 'react' +import type { XLogMeta } from './types' + +import { Collapse } from '~/components/ui/collapse' +import { useIsClient } from '~/hooks/common/use-is-client' +import { useNoteData } from '~/hooks/data/use-note' + +// export const XLogInfoForPost: FC<{ +// id: string +// }> = ({ id }) => { +// const meta = usePostCollection((state) => state.data.get(id)?.meta?.xLog) + +// if (!meta) return null + +// return +// } + +export const XLogInfoForNote: FC = () => { + const data = useNoteData() + + if (!data) return null + + const meta = data.meta?.xLog + return +} + +const XLogInfoBase: FC<{ + meta?: XLogMeta +}> = ({ meta }) => { + const [collapse, setCollapse] = useState(false) + + const isClient = useIsClient() + if (!isClient) return null + + if (!meta) return null + + const { metadata, pageId, cid } = meta as XLogMeta + + const sections = [] as JSX.Element[] + + if (pageId) { + sections.push( +
    +

    Note ID

    + {pageId} +
    , + ) + } + + if (metadata?.owner) { + sections.push( +
    +

    Owner

    + + {metadata.owner} + +
    , + ) + } + + if (metadata?.transactions?.length) { + sections.push( +
    +

    Transaction Hash Creation

    + + {metadata.transactions[0]} + +
    , + ) + } + + if (metadata?.network) { + sections.push( +
    +

    Network

    +

    {metadata.network}

    +
    , + ) + } + + if (cid) { + sections.push( +
    +

    IPFS Address

    + ipfs://{cid} +
    , + ) + } + + return ( +
    +
    { + setCollapse((c) => !c) + }} + > +
    + + + + 此数据所有权由区块链加密技术和智能合约保障仅归创作者所有。 + + + +
    +
    + +
    + {sections} +
    +
    +
    + ) +} + +const SafeIcon = () => ( + + + +) + +const IcRoundKeyboardArrowDown = (props: SVGProps) => { + return ( + + + + ) +} diff --git a/src/components/widgets/xlog/XLogSummary.tsx b/src/components/widgets/xlog/XLogSummary.tsx new file mode 100644 index 0000000000..5a39597458 --- /dev/null +++ b/src/components/widgets/xlog/XLogSummary.tsx @@ -0,0 +1,99 @@ +import { useQuery } from '@tanstack/react-query' +import type { FC, SVGProps } from 'react' + +import { AutoResizeHeight } from '~/components/common/AutoResizeHeight' +import { useNoteData } from '~/hooks/data/use-note' +import { clsxm } from '~/utils/helper' +import { apiClient } from '~/utils/request' + +const XLogSummary: FC<{ + cid: string + className?: string +}> = (props) => { + const { cid } = props + const { data, isLoading, error } = useQuery( + [`getSummary`, cid], + async ({ queryKey }) => { + const [, cid] = queryKey + return apiClient.proxy.fn.xlog.get_summary.get<{ + data: string + }>({ + params: { + cid, + lang: 'zh', + }, + }) + }, + { + enabled: !!cid, + cacheTime: 10000, + }, + ) + + if (!cid) { + return null + } + + return ( +
    +
    + + AI 生成的摘要 +
    + + +

    + {isLoading ? '加载中...' : error ? '请求错误' : data?.data} +

    + {isLoading && ( +

    + (此服务由{' '} + + xLog + {' '} + 驱动) +

    + )} +
    +
    + ) +} + +// export const XLogSummaryForPost: FC<{ +// id: string +// }> = ({ id }) => { +// const cid = usePostCollection((state) => state.data.get(id)?.meta?.xLog?.cid) + +// if (!cid) return null + +// return +// } + +export const XLogSummaryForNote: FC = () => { + const data = useNoteData() + const cid = data?.meta?.xLog?.cid + if (!cid) return null + + return +} + +function OpenAIIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/src/components/widgets/xlog/index.ts b/src/components/widgets/xlog/index.ts new file mode 100644 index 0000000000..7e8d053048 --- /dev/null +++ b/src/components/widgets/xlog/index.ts @@ -0,0 +1,3 @@ +export * from './XLogInfo' +export * from './XLogSummary' +export * from './types' diff --git a/src/components/widgets/xlog/types.ts b/src/components/widgets/xlog/types.ts new file mode 100644 index 0000000000..bfdeb12ee0 --- /dev/null +++ b/src/components/widgets/xlog/types.ts @@ -0,0 +1,21 @@ +export interface XLogMeta { + pageId?: string + cid?: string + relatedUrls?: string[] + metadata?: { + network: string + proof: string + raw?: { + [key: string]: any + } + owner?: string + transactions?: string[] + [key: string]: any + } +} + +declare module '@mx-space/api-client' { + export interface PostMeta { + xLog?: XLogMeta + } +} diff --git a/src/lib/route-builder.ts b/src/lib/route-builder.ts new file mode 100644 index 0000000000..0ce17d5dae --- /dev/null +++ b/src/lib/route-builder.ts @@ -0,0 +1,57 @@ +export enum Routes { + Home = '/home', + Posts = '/posts', + Post = '/posts/', + Notes = '/notes', + Note = '/notes/', + Timelime = '/timeline', +} + +type Noop = never +type Pagination = { + size?: number + page?: number +} + +type WithId = { + id: string | number +} +type HomeParams = Noop +type PostsParams = Pagination +type PostParams = { + category: string + slug: string +} +type NotesParams = Noop +type NoteParams = WithId & { + password?: string +} + +export type RouteParams = T extends Routes.Home + ? HomeParams + : T extends Routes.Note + ? NoteParams + : T extends Routes.Notes + ? NotesParams + : T extends Routes.Posts + ? PostsParams + : T extends Routes.Post + ? PostParams + : never + +export const routeBuilder = ( + route: T, + params: RouteParams, +) => { + const href = route + switch (route) { + case Routes.Note: { + route + params + // ^? + break + } + case Routes.Home: { + } + } +} diff --git a/src/styles/image-zoom.css b/src/styles/image-zoom.css new file mode 100644 index 0000000000..8667ee92e3 --- /dev/null +++ b/src/styles/image-zoom.css @@ -0,0 +1,16 @@ +/* image-zoom */ +.medium-zoom-overlay { + z-index: 9; +} + +.medium-zoom-overlay + .medium-zoom-image { + z-index: 10; +} + +.medium-zoom-image { + border-radius: 0.5rem; + transition: border-radius 0.3s ease-in-out; +} +.medium-zoom-image.medium-zoom-image--opened { + border-radius: 0; +} diff --git a/src/styles/index.css b/src/styles/index.css index 232f6bf0dd..4b1e1a00dc 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -4,3 +4,4 @@ @import './scrollbar.css'; @import './print.css'; @import './clerk.css'; +@import './image-zoom.css'; diff --git a/src/styles/print.css b/src/styles/print.css index fd0ea11d42..7a18aef73c 100644 --- a/src/styles/print.css +++ b/src/styles/print.css @@ -1,3 +1,5 @@ -[data-hide-print] { - display: none !important; +@media print { + [data-hide-print] { + display: none !important; + } } diff --git a/src/styles/scrollbar.css b/src/styles/scrollbar.css index 00cbf2314a..b1b31830ba 100644 --- a/src/styles/scrollbar.css +++ b/src/styles/scrollbar.css @@ -2,6 +2,10 @@ body::-webkit-scrollbar { height: 0; } +body { + overflow: overlay; +} + ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb:hover { background-color: theme(colors.neutral); @@ -10,8 +14,8 @@ body::-webkit-scrollbar { } ::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 5px !important; + height: 5px !important; background: theme(colors.base-100); } diff --git a/src/utils/scroller.ts b/src/utils/scroller.ts index aa4129fecf..8a341e5edc 100644 --- a/src/utils/scroller.ts +++ b/src/utils/scroller.ts @@ -4,14 +4,23 @@ import { animateValue } from 'framer-motion' import { microdampingPreset } from '~/constants/spring' +// TODO scroller lock export const springScrollTo = (y: number) => { const scrollTop = // FIXME latest version framer will ignore keyframes value `0` document.documentElement.scrollTop || document.body.scrollTop + + const stopSpringScrollHandler = () => { + animation.stop() + } const animation = animateValue({ keyframes: [scrollTop + 1, y], autoplay: true, ...microdampingPreset, + onPlay() { + window.addEventListener('wheel', stopSpringScrollHandler) + window.addEventListener('touchmove', stopSpringScrollHandler) + }, onUpdate(latest) { if (latest <= 0) { @@ -20,6 +29,11 @@ export const springScrollTo = (y: number) => { window.scrollTo(0, latest) }, }) + + animation.then(() => { + window.removeEventListener('wheel', stopSpringScrollHandler) + window.removeEventListener('touchmove', stopSpringScrollHandler) + }) return animation } diff --git a/tailwind.config.ts b/tailwind.config.ts index 0170106894..6cb931b57a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -287,6 +287,12 @@ function addShortcutPlugin({ addUtilities }: PluginAPI) { 'box-shadow': '0 0 10px rgb(120 120 120 / 10%), 0 5px 20px rgb(120 120 120 / 20%)', }, + '.backface-hidden': { + '-webkit-backface-visibility': 'hidden', + '-moz-backface-visibility': 'hidden', + '-webkit-transform': 'translate3d(0, 0, 0)', + '-moz-transform': 'translate3d(0, 0, 0)', + }, } addUtilities(styles) }