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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!!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 ? (
+
+
![]({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}]({src})
-
- >
- )
-})
-
-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 ? (
-
- ) : (
-
-
-
- {!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(
+ ,
+ )
+ }
+
+ if (metadata?.owner) {
+ sections.push(
+ ,
+ )
+ }
+
+ if (metadata?.transactions?.length) {
+ sections.push(
+ ,
+ )
+ }
+
+ if (metadata?.network) {
+ sections.push(
+
+ Network
+ {metadata.network}
+ ,
+ )
+ }
+
+ if (cid) {
+ sections.push(
+ ,
+ )
+ }
+
+ 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)
}