Skip to content

Commit

Permalink
feat: image transition
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jun 17, 2023
1 parent c9c678d commit efea201
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 40 deletions.
8 changes: 7 additions & 1 deletion src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Balancer } from 'react-wrap-balancer'
import clsx from 'clsx'
import dayjs from 'dayjs'
import { useParams } from 'next/navigation'
import type { Image } from '@mx-space/api-client'
import type { MarkdownToJSX } from '~/components/ui/markdown'

import { PageDataHolder } from '~/components/common/PageHolder'
Expand All @@ -15,11 +16,14 @@ import { Toc, TocAutoScroll } from '~/components/widgets/toc'
import { useBeforeMounted } from '~/hooks/common/use-before-mounted'
import { useNoteByNidQuery } from '~/hooks/data/use-note'
import { ArticleElementProvider } from '~/providers/article/article-element-provider'
import { MarkdownImageRecordProvider } from '~/providers/article/markdown-image-record-provider'
import { useSetCurrentNoteId } from '~/providers/note/current-note-id-provider'
import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'

import styles from './page.module.css'

const noopArr = [] as Image[]

const PageImpl = () => {
const { id } = useParams() as { id: string }
const { data } = useNoteByNidQuery(id)
Expand Down Expand Up @@ -72,7 +76,9 @@ const PageImpl = () => {
</header>

<ArticleElementProvider>
<Markdown as="main" renderers={Markdownrenderers} value={note.text} />
<MarkdownImageRecordProvider images={note.images || noopArr}>
<Markdown as="main" renderers={Markdownrenderers} value={note.text} />
</MarkdownImageRecordProvider>

<NoteLayoutRightSidePortal>
<Toc className="sticky top-[120px] ml-4 mt-[120px]" />
Expand Down
11 changes: 11 additions & 0 deletions src/components/common/ProviderComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import React from 'react'

export const ProviderComposer: Component<{
contexts: JSX.Element[]
}> = ({ contexts, children }) => {
return contexts.reduceRight((kids: any, parent: any) => {
return React.cloneElement(parent, { children: kids })
}, children)
}
2 changes: 1 addition & 1 deletion src/components/ui/float-popover/FloatPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export const FloatPopover: FC<
aria-modal="true"
className={clsxm(
'!shadow-out-sm focus:!shadow-out-sm focus-visible:!shadow-out-sm',
'rounded-xl border border-zinc-400/20 bg-base-100/80 p-4 shadow-lg outline-none backdrop-blur-lg dark:border-zinc-500/30',
'bg-base-100/98 rounded-xl border border-zinc-400/20 p-4 shadow-lg outline-none backdrop-blur-lg dark:border-zinc-500/30',

headless && styles['headless'],
animate && styles['animate'],
Expand Down
128 changes: 109 additions & 19 deletions src/components/ui/image/ZoomedImage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use client'

import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { motion, useAnimationControls } from 'framer-motion'
import { tv } from 'tailwind-variants'
import type { ReactNode } from 'react'
import type { FC, ReactNode } from 'react'

import { LazyLoad } from '~/components/common/Lazyload'
import { useIsUnMounted } from '~/hooks/common/use-is-unmounted'
import { calculateDimensions } from '~/lib/calc-image'
import { useArticleElementSize } from '~/providers/article/article-element-provider'
import { useMarkdownImageRecord } from '~/providers/article/markdown-image-record-provider'
import { clsxm } from '~/utils/helper'

import { Divider } from '../divider'

Expand All @@ -28,7 +33,7 @@ export enum ImageLoadStatus {
}

const styles = tv({
base: '',
base: 'rounded-xl overflow-hidden text-center inline-flex items-center justify-center duration-200',
variants: {
status: {
loading: 'hidden opacity-0',
Expand All @@ -37,11 +42,12 @@ const styles = tv({
},
},
})

export const ImageLazy: Component<TImageProps & BaseImageProps> = ({
alt,
src,
title,
accent,

placeholder,
}) => {
const figcaption = title || alt
Expand All @@ -57,33 +63,117 @@ export const ImageLazy: Component<TImageProps & BaseImageProps> = ({
},
[isUnmount],
)

const controls = useAnimationControls()

return (
<LazyLoad placeholder={placeholder}>
<figure>
<img
src={src}
title={title}
alt={alt}
onLoad={() => setImageLoadStatusSafe(ImageLoadStatus.Loaded)}
onError={() => setImageLoadStatusSafe(ImageLoadStatus.Error)}
className={styles({
status: imageLoadStatus,
})}
/>
<figcaption className="mt-1 flex flex-col items-center justify-center">
<Divider className="w-[80px] opacity-80" />
<span>{figcaption}</span>
</figcaption>
<span className="relative block">
<span>
{imageLoadStatus !== ImageLoadStatus.Loaded && placeholder}
</span>
<motion.img
variants={{
loading: {
opacity: 0,
// filter: 'blur(10px)',
},
loaded: {
opacity: 1,
// filter: 'blur(0px)',
},
}}
initial="loading"
animate={controls}
src={src}
title={title}
alt={alt}
onLoad={() => {
setImageLoadStatusSafe(ImageLoadStatus.Loaded)
requestAnimationFrame(() => {
controls.start('loaded')
})
}}
onError={() => setImageLoadStatusSafe(ImageLoadStatus.Error)}
className={styles({
status: imageLoadStatus,
})}
/>
</span>

{!!figcaption && (
<figcaption className="mt-1 flex flex-col items-center justify-center">
<Divider className="w-[80px] opacity-80" />
<span>{figcaption}</span>
</figcaption>
)}
</figure>
</LazyLoad>
)
}

export const ZoomedImage: Component<TImageProps> = (props) => {
console.log(props)
return (
<span className="block text-center">
<ImageLazy {...props} zoom />
</span>
)
}

export const FixedZoomedImage: Component<TImageProps> = (props) => {
const placeholder = useMemo(() => {
return <Placeholder src={props.src} />
}, [props.src])
return <ImageLazy zoom placeholder={placeholder} {...props} />
}

const Placeholder: FC<{
src: string
}> = ({ src }) => {
const { h, w } = useArticleElementSize()
const imageMeta = useMarkdownImageRecord(src)

const scaledSize = useMemo(() => {
if (!h || !w) return
if (!imageMeta) return
const { height, width } = imageMeta
const { height: scaleHeight, width: scaleWidth } = calculateDimensions(
width,
height,
{
width: w,
height: Infinity,
},
)

return {
scaleHeight,
scaleWidth,
}
}, [h, w, imageMeta])

if (!scaledSize) return <NoFixedPlaceholder />
if (h === 0 || w === 0) return <NoFixedPlaceholder />
return (
<span
className={styles.base}
style={{
height: scaledSize.scaleHeight,
width: scaledSize.scaleWidth,
backgroundColor: imageMeta?.accent,
}}
/>
)
}

const NoFixedPlaceholder = () => {
return (
<span
className={clsxm(
styles.base,
'h-[150px] w-full bg-slate-300 dark:bg-slate-700',
)}
/>
)
}
4 changes: 2 additions & 2 deletions src/components/ui/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { compiler } from 'markdown-to-jsx'
import type { MarkdownToJSX } from 'markdown-to-jsx'
import type { FC, PropsWithChildren } from 'react'

import { FixedZoomedImage } from '../image'
import styles from './index.module.css'
import { CommentAtRule } from './parsers/comment-at'
import { ContainerRule } from './parsers/container'
Expand All @@ -16,7 +17,6 @@ import { SpoilderRule } from './parsers/spoiler'
import { MParagraph, MTableBody, MTableHead, MTableRow } from './renderers'
import { MDetails } from './renderers/collapse'
import { MFootNote } from './renderers/footnotes'
import { ZoomedImage } from '../image'

export interface MdProps {
value?: string
Expand Down Expand Up @@ -66,7 +66,7 @@ export const Markdown: FC<MdProps & MarkdownToJSX.Options & PropsWithChildren> =
// FIXME: footer tag in raw html will renders not as expected, but footer tag in this markdown lib will wrapper as linkReferer footnotes
footer: MFootNote,
details: MDetails,
img: ZoomedImage,
img: FixedZoomedImage,

// for custom react component
// LinkCard,
Expand Down
1 change: 1 addition & 0 deletions src/lib/fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const sansFont = Manrope({
variable: '--font-sans',
display: 'swap',
})

const serifFont = Noto_Serif_SC({
subsets: ['latin'],
weight: ['400'],
Expand Down
18 changes: 10 additions & 8 deletions src/providers/article/article-element-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { memo, useEffect, useRef } from 'react'
import { createContextState } from 'foxact/create-context-state'
import { useIsomorphicLayoutEffect } from 'foxact/use-isomorphic-layout-effect'

import { ProviderComposer } from '~/components/common/ProviderComposer'
import { clsxm } from '~/utils/helper'

const [
Expand All @@ -25,16 +26,17 @@ const [
useSetIsEOArticleElement,
] = createContextState<boolean>(false)

const Providers = [
<ArticleElementProviderInternal key="ArticleElementProviderInternal" />,
<ArticleElementSizeProviderInternal key="ArticleElementSizeProviderInternal" />,
<IsEOArticleElementProviderInternal key="IsEOArticleElementProviderInternal" />,
]
const ArticleElementProvider: Component = ({ children, className }) => {
return (
<ArticleElementProviderInternal>
<ArticleElementSizeProviderInternal>
<IsEOArticleElementProviderInternal>
<ArticleElementResizeObserver />
<Content className={className}>{children}</Content>
</IsEOArticleElementProviderInternal>
</ArticleElementSizeProviderInternal>
</ArticleElementProviderInternal>
<ProviderComposer contexts={Providers}>
<ArticleElementResizeObserver />
<Content className={className}>{children}</Content>
</ProviderComposer>
)
}
const ArticleElementResizeObserver = () => {
Expand Down
44 changes: 44 additions & 0 deletions src/providers/article/markdown-image-record-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
} from 'react'
import { atom, useAtomValue } from 'jotai'
import { selectAtom } from 'jotai/utils'
import type { Image } from '@mx-space/api-client'

import { jotaiStore } from '~/lib/store'

const MarkdownImageRecordProviderInternal = createContext(atom([] as Image[]))

export const MarkdownImageRecordProvider: Component<{
images: Image[]
}> = ({ children, images }) => {
const atomRef = useRef(atom([...images] as Image[])).current

useEffect(() => {
jotaiStore.set(atomRef, [...images])
}, [images])

return (
<MarkdownImageRecordProviderInternal.Provider value={atomRef}>
{children}
</MarkdownImageRecordProviderInternal.Provider>
)
}

export const useMarkdownImageRecord = (src: string) => {
return useAtomValue(
selectAtom(
useContext(MarkdownImageRecordProviderInternal),
useCallback(
(value: Image[]) => {
return value.find((image) => image.src === src)
},
[src],
),
),
)
}
10 changes: 1 addition & 9 deletions src/providers/root/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
'use client'

import { ReactQueryProvider } from './react-query-provider'
import React from 'react'
import { ThemeProvider } from 'next-themes'
import type { PropsWithChildren } from 'react'

import { ProviderComposer } from '../../components/common/ProviderComposer'
import { AggregationProvider } from './aggregation-data-provider'
import { DebugProvider } from './debug-provider'
import { JotaiStoreProvider } from './jotai-provider'
import { PageScrollInfoProvider } from './page-scroll-info-provider'
import { SocketProvider } from './socket-provider'
import { ViewportProvider } from './viewport-provider'

const ProviderComposer: Component<{
contexts: JSX.Element[]
}> = ({ contexts, children }) => {
return contexts.reduceRight((kids: any, parent: any) => {
return React.cloneElement(parent, { children: kids })
}, children)
}

const contexts: JSX.Element[] = [
<ThemeProvider key="themeProvider" />,
<ReactQueryProvider key="reactQueryProvider" />,
Expand Down

0 comments on commit efea201

Please sign in to comment.