Skip to content

Commit

Permalink
feat: note action bar
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Jun 18, 2023
1 parent cabf021 commit dffd250
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 29 deletions.
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../styles/index.css'

import { dehydrate } from '@tanstack/react-query'
import { ToastContainer } from 'react-toastify'

import { ClerkProvider } from '@clerk/nextjs'

Expand Down Expand Up @@ -90,6 +91,7 @@ export default async function RootLayout(props: Props) {
<Root>{children}</Root>
</Hydrate>
</Providers>
<ToastContainer />
</body>
</html>
</ClerkProvider>
Expand Down
10 changes: 8 additions & 2 deletions src/app/notes/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ 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 { TocAside, 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'
Expand All @@ -30,6 +30,7 @@ import { NoteLayoutRightSidePortal } from '~/providers/note/right-side-provider'
import { parseDate } from '~/utils/datetime'
import { springScrollToTop } from '~/utils/scroller'

import { NoteActionAside } from '../../../components/widgets/note/NoteActionAside'
import styles from './page.module.css'

const noopArr = [] as Image[]
Expand Down Expand Up @@ -96,7 +97,12 @@ const PageImpl = () => {
</MarkdownImageRecordProvider>

<NoteLayoutRightSidePortal>
<Toc className="sticky top-[120px] ml-4 mt-[120px]" />
<TocAside
className="sticky top-[120px] ml-4 mt-[120px]"
treeClassName="max-h-[calc(100vh-6rem-4.5rem-300px)] h-[calc(100vh-6rem-4.5rem-300px)]"
>
<NoteActionAside />
</TocAside>
<TocAutoScroll />
</NoteLayoutRightSidePortal>
</ArticleElementProvider>
Expand Down
47 changes: 47 additions & 0 deletions src/components/common/ToastCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import clsx from 'clsx'
import type { FC } from 'react'
import type { ToastProps, TypeOptions } from 'react-toastify/dist/types'

import { MotionButtonBase } from '../ui/button'

const typeMap: Record<TypeOptions, JSX.Element> = {
success: <i className="icon-[mingcute--check-fill] text-uk-green-light" />,
error: <i className="icon-[mingcute--close-fill] text-uk-red-light" />,
info: <i className="icon-[mingcute--information-fill] text-uk-blue-light" />,
warning: <i className="icon-[mingcute--alert-fill] text-uk-orange-light" />,
default: (
<i className="icon-[mingcute--information-fill] text-uk-blue-light" />
),
}

export const ToastCard: FC<{
message: string
toastProps?: ToastProps
iconElement?: JSX.Element
closeToast?: () => void
}> = (props) => {
const { iconElement, message, closeToast } = props

return (
<div
className={clsx(
'relative w-full overflow-hidden rounded-xl shadow-out-sm',
'my-4 mr-4 px-4 py-5',
'bg-base-100/90 backdrop-blur-sm',
'border border-slate-100/80 dark:border-neutral-900/80',
'space-x-4',
'flex items-center',
)}
>
{iconElement ?? typeMap[props.toastProps?.type ?? 'default']}
<span>{message}</span>

<MotionButtonBase
className="absolute bottom-0 right-3 top-0 flex items-center text-sm text-base-content/40 duration-200 hover:text-base-content/80"
onClick={closeToast}
>
<i className="icon-[mingcute--close-fill] p-2" />
</MotionButtonBase>
</div>
)
}
69 changes: 69 additions & 0 deletions src/components/widgets/note/NoteActionAside.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use client'

import { useQueryClient } from '@tanstack/react-query'
import { motion } from 'framer-motion'
import { produce } from 'immer'
import type { NoteWrappedPayload } from '@mx-space/api-client'

import { MotionButtonBase } from '~/components/ui/button'
import { microReboundPreset } from '~/constants/spring'
import { useNoteData } from '~/hooks/data/use-note'
import { toast } from '~/lib/toast'
import { queries } from '~/queries/definition'
import { apiClient } from '~/utils/request'

export const NoteActionAside = () => {
return (
<div className="absolute bottom-0 max-h-[300px] flex-col space-y-4">
<LikeButton />
</div>
)
}

const LikeButton = () => {
const note = useNoteData()

const queryClient = useQueryClient()
if (!note) return null
const id = note.id
const handleLike = () => {
apiClient.note.likeIt(id).then(() => {
queryClient.setQueriesData(
queries.note.byNid(note.nid.toString()),
(old: any) => {
return produce(old as NoteWrappedPayload, (draft) => {
draft.data.count.like += 1
})
},
)
})
}
return (
<MotionButtonBase
className="flex flex-col space-y-2"
onClick={() => {
handleLike()
toast('谢谢你!', undefined, {
iconElement: (
<motion.i
className="icon-[mingcute--heart-fill] text-uk-red-light"
initial={{
scale: 0.96,
}}
animate={{
scale: 1.12,
}}
transition={{
...microReboundPreset,
delay: 1,
repeat: 5,
}}
/>
),
})
}}
>
<i className="icon-[mingcute--heart-fill] text-[24px] opacity-80 duration-200 hover:text-uk-red-light hover:opacity-100" />
</MotionButtonBase>
)
}
86 changes: 60 additions & 26 deletions src/components/widgets/toc/Toc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import { springScrollToElement } from '~/utils/scroller'

import { TocItem } from './TocItem'

export type TocProps = {
useAsWeight?: boolean
export type TocAsideProps = {
treeClassName?: string
}

export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
export const TocAside: Component<TocAsideProps> = ({
className,
children,
treeClassName,
}) => {
const containerRef = useRef<HTMLUListElement>(null)
const $article = useArticleElement()

Expand Down Expand Up @@ -48,9 +52,6 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
}, [$headings])

useEffect(() => {
if (useAsWeight) {
return
}
const setMaxWidth = throttle(() => {
if (containerRef.current) {
containerRef.current.style.maxWidth = `${
Expand All @@ -66,7 +67,7 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
return () => {
window.removeEventListener('resize', setMaxWidth)
}
}, [useAsWeight])
}, [])

const rootDepth = useMemo(
() =>
Expand All @@ -81,6 +82,37 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {

const [activeId, setActiveId] = useActiveId($headings)

return (
<aside className={clsxm('st-toc z-[3]', 'relative font-sans', className)}>
<TocTree
toc={toc}
activeId={activeId}
setActiveId={setActiveId}
rootDepth={rootDepth}
containerRef={containerRef}
className={clsxm('absolute max-h-[75vh]', treeClassName)}
>
{children}
</TocTree>
</aside>
)
}

const TocTree: Component<{
toc: ITocItem[]
activeId: string | null
setActiveId: (id: string | null) => void
rootDepth: number
containerRef: React.MutableRefObject<HTMLUListElement | null>
}> = ({
toc,
activeId,
setActiveId,
rootDepth,
containerRef,
className,
children,
}) => {
const handleScrollTo = useCallback(
(i: number, $el: HTMLElement | null, anchorId: string) => {
if ($el) {
Expand All @@ -92,25 +124,27 @@ export const Toc: Component<TocProps> = ({ useAsWeight, className }) => {
[],
)
return (
<aside className={clsxm('st-toc z-[3]', 'relative font-sans', className)}>
<ul
className="absolute max-h-[75vh] overflow-y-auto px-2 font-medium scrollbar-none"
key={`${toc.map((i) => i.title).join('')}`}
ref={containerRef}
>
{toc?.map((heading) => {
return (
<MemoedItem
heading={heading}
isActive={heading.anchorId === activeId}
key={heading.title}
rootDepth={rootDepth}
onClick={handleScrollTo}
/>
)
})}
</ul>
</aside>
<ul
className={clsxm(
'overflow-y-auto px-2 font-medium scrollbar-none',
className,
)}
key={`${toc.map((i) => i.title).join('')}`}
ref={containerRef}
>
{toc?.map((heading) => {
return (
<MemoedItem
heading={heading}
isActive={heading.anchorId === activeId}
key={heading.title}
rootDepth={rootDepth}
onClick={handleScrollTo}
/>
)
})}
{children}
</ul>
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/widgets/xlog/XLogSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const XLogSummary: FC<{
return (
<div
className={clsxm(
`mt-4 space-y-2 rounded-xl border border-slate-100 p-4 dark:border-slate-900`,
`mt-4 space-y-2 rounded-xl border border-slate-100 p-4 dark:border-neutral-800`,
props.className,
)}
>
Expand Down
26 changes: 26 additions & 0 deletions src/lib/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createElement } from 'react'
import { toast as Toast } from 'react-toastify'
import type { ToastOptions, TypeOptions } from 'react-toastify'

import { ToastCard } from '~/components/common/ToastCard'

export const toast = (
message: string,
type?: TypeOptions,
options?: ToastOptions & {
iconElement?: JSX.Element
},
) => {
const { iconElement, ...rest } = options || {}
return Toast(createElement(ToastCard, { message, iconElement }), {
type,
position: 'bottom-right',
autoClose: 3000,
pauseOnHover: true,
hideProgressBar: true,

closeOnClick: true,
closeButton: false,
...rest,
})
}
1 change: 1 addition & 0 deletions src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
@import './print.css';
@import './clerk.css';
@import './image-zoom.css';
@import './toastify.css';
Loading

0 comments on commit dffd250

Please sign in to comment.