Skip to content

Commit

Permalink
feat: to top
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei committed Jun 16, 2023
1 parent 464b9e9 commit 3742f6c
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 10 deletions.
5 changes: 5 additions & 0 deletions src/components/layout/root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BackToTopFAB, FABContainer } from '~/components/ui/fab'

import { Content } from '../content/Content'
import { Footer } from '../footer'
import { Header } from '../header'
Expand All @@ -9,6 +11,9 @@ export const Root: Component = ({ children }) => {
<Content>{children}</Content>

<Footer />
<FABContainer>
<BackToTopFAB />
</FABContainer>
</>
)
}
28 changes: 28 additions & 0 deletions src/components/ui/fab/BackToTopFAB.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import { useViewport } from '~/atoms'
import { usePageScrollLocationSelector } from '~/providers/root/page-scroll-info-provider'
import { springScrollToTop } from '~/utils/spring'

import { FABBase } from './FABContainer'

export const BackToTopFAB = () => {
const windowHeight = useViewport((v) => v.h)
const shouldShow = usePageScrollLocationSelector(
(scrollTop) => {
return scrollTop > windowHeight / 5
},
[windowHeight],
)

return (
<FABBase
id="to-top"
aria-label="Back to top"
show={shouldShow}
onClick={springScrollToTop}
>
<i className="icon-[mingcute--arow-to-up-line]" />
</FABBase>
)
}
146 changes: 146 additions & 0 deletions src/components/ui/fab/FABContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
'use client'

import React, { useEffect, useState } from 'react'
import type { PropsWithChildren } from 'react'

import { useStateToRef } from '~/hooks/common/use-state-ref'
import { clsxm } from '~/utils/helper'

export interface FABConfig {
id: string
icon: JSX.Element
onClick: () => void
}

class FABStatic {
private setState: React.Dispatch<React.SetStateAction<FABConfig[]>> | null =
null
register(setter: any) {
this.setState = setter
}
destroy() {
this.setState = null
}

add(fabConfig: FABConfig) {
if (!this.setState) return

const id = fabConfig.id

this.setState((state) => {
if (state.find((config) => config.id === id)) return state
return [...state, fabConfig]
})

return () => {
this.remove(fabConfig.id)
}
}

remove(id: string) {
if (!this.setState) return
this.setState((state) => {
return state.filter((config) => config.id !== id)
})
}
}

const fab = new FABStatic()

export const useFAB = (fabConfig: FABConfig) => {
useEffect(() => {
return fab.add(fabConfig)
}, [])
}

export const FABBase = (
props: PropsWithChildren<
{
id: string
show?: boolean
children: JSX.Element
} & React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
>,
) => {
const { children, show = true, ...extra } = props
const { className, onTransitionEnd, ...rest } = extra

const [mounted, setMounted] = useState(true)
const [appearTransition, setAppearTransition] = useState(false)
const getMounted = useStateToRef(mounted)
const handleTransitionEnd: React.TransitionEventHandler<HTMLButtonElement> = (
e,
) => {
onTransitionEnd?.(e)

!show && setMounted(false)
}

useEffect(() => {
if (show && !getMounted.current) {
setAppearTransition(true)
setMounted(true)

requestAnimationFrame(() => {
setAppearTransition(false)
})
}
}, [show])

return (
<button
className={clsxm(
'mt-2 inline-flex h-8 w-8 items-center justify-center rounded-md border border-accent bg-white text-accent opacity-50 transition-all duration-300 hover:opacity-100 focus:opacity-100 focus:outline-none',
(!show || appearTransition) && 'translate-x-[60px]',
!mounted && 'hidden',
className,
)}
onTransitionEnd={handleTransitionEnd}
{...rest}
>
{children}
</button>
)
}

export const FABContainer = (props: {
children: JSX.Element | JSX.Element[]
}) => {
const [fabConfig, setFabConfig] = useState<FABConfig[]>([])
useEffect(() => {
fab.register(setFabConfig)
return () => {
fab.destroy()
}
}, [])

const [serverSide, setServerSide] = useState(true)

useEffect(() => {
setServerSide(false)
}, [])

if (serverSide) return null

return (
<div
data-testid="fab-container"
className={clsxm(
'font-lg fixed bottom-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] right-4 z-[9] flex flex-col',
)}
>
{fabConfig.map((config) => {
const { id, onClick, icon } = config
return (
<FABBase id={id} onClick={onClick} key={id}>
{icon}
</FABBase>
)
})}
{props.children}
</div>
)
}
2 changes: 2 additions & 0 deletions src/components/ui/fab/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './BackToTopFAB'
export * from './FABContainer'
21 changes: 12 additions & 9 deletions src/components/ui/markdown/parsers/mention.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { MarkdownToJSX } from 'markdown-to-jsx'
import { Priority, simpleInlineRegex } from 'markdown-to-jsx'
import React from 'react'
import { CodiconGithubInverted, MdiTwitter, IcBaselineTelegram } from '~/components/icons/menu-collection'
import { Priority, simpleInlineRegex } from 'markdown-to-jsx'
import type { MarkdownToJSX } from 'markdown-to-jsx'

import {
CodiconGithubInverted,
IcBaselineTelegram,
MdiTwitter,
} from '~/components/icons/menu-collection'


const prefixToIconMap = {
GH: <CodiconGithubInverted />,
TW: <MdiTwitter />,
Expand Down Expand Up @@ -42,17 +46,16 @@ export const MentionRule: MarkdownToJSX.Rule = {
const { prefix, name } = content
if (!name) {
return null as any

}

// @ts-ignore
const Icon = prefixToIconMap[prefix]
// @ts-ignore
const urlPrefix = prefixToUrlMap[prefix]
const urlPrefix = prefixToUrlMap[prefix]

return (
<div
className="mr-2 inline-flex items-center space-x-2 align-bottom"
<span
className="mx-1 inline-flex items-center space-x-1 align-bottom"
key={state?.key}
>
{Icon}
Expand All @@ -63,7 +66,7 @@ export const MentionRule: MarkdownToJSX.Rule = {
>
{name}
</a>
</div>
</span>
)
},
}
3 changes: 2 additions & 1 deletion src/providers/root/page-scroll-info-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ const usePageScrollLocation = () => useAtomValue(pageScrollLocationAtom)
const usePageScrollDirection = () => useAtomValue(pageScrollDirectionAtom)
const usePageScrollLocationSelector = <T,>(
selector: (scrollY: number) => T,
deps: any[] = [],
): T =>
useAtomValue(
// @ts-ignore
selectAtom(
pageScrollLocationAtom,
useCallback(($) => selector($), []),
useCallback(($) => selector($), deps),
),
)
export {
Expand Down

1 comment on commit 3742f6c

@vercel
Copy link

@vercel vercel bot commented on 3742f6c Jun 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

springtide – ./

springtide.vercel.app
springtide-git-main-innei.vercel.app
springtide-innei.vercel.app

Please sign in to comment.