Co-authored-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
b56a175dcd
commit
c387a631cd
|
|
@ -15,6 +15,8 @@ import {
|
|||
|
||||
import { CharacterCard } from "~/components/common/CharacterCard"
|
||||
|
||||
import { Portal } from "./Portal"
|
||||
|
||||
export const CharacterFloatCard: React.FC<{
|
||||
siteId?: string
|
||||
children: JSX.Element
|
||||
|
|
@ -27,6 +29,7 @@ export const CharacterFloatCard: React.FC<{
|
|||
onOpenChange: setOpen,
|
||||
middleware: [offset(5), flip(), shift({ padding: 8 })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: "fixed",
|
||||
})
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
|
|
@ -51,25 +54,27 @@ export const CharacterFloatCard: React.FC<{
|
|||
{children}
|
||||
</span>
|
||||
{isMounted && (
|
||||
<span
|
||||
ref={refs.setFloating}
|
||||
className={
|
||||
"z-10 block w-80" + (open || buttonLoading ? "" : " hidden")
|
||||
}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? "0",
|
||||
left: x ?? "0",
|
||||
...styles,
|
||||
}}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<CharacterCard
|
||||
siteId={siteId}
|
||||
open={open}
|
||||
setButtonLoading={setButtonLoading}
|
||||
/>
|
||||
</span>
|
||||
<Portal>
|
||||
<span
|
||||
ref={refs.setFloating}
|
||||
className={
|
||||
"z-10 block w-80" + (open || buttonLoading ? "" : " hidden")
|
||||
}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? "0",
|
||||
left: x ?? "0",
|
||||
...styles,
|
||||
}}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<CharacterCard
|
||||
siteId={siteId}
|
||||
open={open}
|
||||
setButtonLoading={setButtonLoading}
|
||||
/>
|
||||
</span>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
import React from "react"
|
||||
import React, { useRef } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
|
||||
import { Button } from "../ui/Button"
|
||||
import CharacterListItem from "./CharacterListItem"
|
||||
import { PortalProvider } from "./Portal"
|
||||
|
||||
export const CharacterList: React.FC<{
|
||||
open: boolean
|
||||
|
|
@ -23,26 +24,40 @@ export const CharacterList: React.FC<{
|
|||
[] as any[],
|
||||
) as any[]
|
||||
|
||||
const $modalEl = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpen} title={title} zIndex={20}>
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title={title}
|
||||
zIndex={20}
|
||||
ref={$modalEl}
|
||||
>
|
||||
{list?.length ? (
|
||||
<Virtuoso
|
||||
overscan={10}
|
||||
style={{ height: flattenList.length * 64 }}
|
||||
fixedItemHeight={64}
|
||||
className="max-h-screen"
|
||||
endReached={() => hasMore && loadMore()}
|
||||
components={{
|
||||
Footer: hasMore ? Loader : undefined,
|
||||
}}
|
||||
data={flattenList}
|
||||
itemContent={(index, sub) => {
|
||||
const character = sub?.character || sub?.fromCharacter
|
||||
return (
|
||||
<CharacterListItem key={index} sub={sub} character={character} />
|
||||
)
|
||||
}}
|
||||
></Virtuoso>
|
||||
<PortalProvider to={$modalEl.current!}>
|
||||
<Virtuoso
|
||||
overscan={10}
|
||||
style={{ height: flattenList.length * 64 }}
|
||||
fixedItemHeight={64}
|
||||
className="max-h-screen"
|
||||
endReached={() => hasMore && loadMore()}
|
||||
components={{
|
||||
Footer: hasMore ? Loader : undefined,
|
||||
}}
|
||||
data={flattenList}
|
||||
itemContent={(index, sub) => {
|
||||
const character = sub?.character || sub?.fromCharacter
|
||||
return (
|
||||
<CharacterListItem
|
||||
key={index}
|
||||
sub={sub}
|
||||
character={character}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
></Virtuoso>
|
||||
</PortalProvider>
|
||||
) : (
|
||||
<div className="px-5 overflow-auto flex-1">
|
||||
<div className="py-3 text-center text-zinc-300">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
import { PropsWithChildren, createContext, useContext, useMemo } from "react"
|
||||
import { createPortal } from "react-dom"
|
||||
|
||||
import { useIsClient } from "~/hooks/useClient"
|
||||
import { isServerSide } from "~/lib/utils"
|
||||
|
||||
export const usePortal = () => {
|
||||
const ctx = useContext(RootPortalContext)
|
||||
if (isServerSide()) {
|
||||
return null
|
||||
}
|
||||
return ctx.to || document.body
|
||||
}
|
||||
|
||||
const RootPortalContext = createContext<{
|
||||
to?: HTMLElement | undefined
|
||||
}>({
|
||||
to: undefined,
|
||||
})
|
||||
export const PortalProvider = (
|
||||
props: PropsWithChildren<{
|
||||
to: HTMLElement
|
||||
}>,
|
||||
) => (
|
||||
<RootPortalContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
to: props.to,
|
||||
}),
|
||||
[props.to],
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</RootPortalContext.Provider>
|
||||
)
|
||||
|
||||
export const Portal = (
|
||||
props: PropsWithChildren<{
|
||||
to?: HTMLElement
|
||||
}>,
|
||||
) => {
|
||||
const client = useIsClient()
|
||||
const to = usePortal()
|
||||
|
||||
if (!client) return null
|
||||
|
||||
return createPortal(props.children, props.to || to || document.body)
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import clsx from "clsx"
|
||||
import React from "react"
|
||||
import React, { forwardRef } from "react"
|
||||
|
||||
import { Dialog } from "@headlessui/react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
export const Modal: React.FC<{
|
||||
interface ModalProps {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
|
|
@ -13,49 +13,58 @@ export const Modal: React.FC<{
|
|||
titleIcon?: React.ReactNode
|
||||
size?: "md" | "lg" | "sm"
|
||||
zIndex?: number
|
||||
}> = ({ open, setOpen, children, title, titleIcon, size = "md", zIndex }) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
className={clsx("relative", zIndex ? `z-${zIndex}` : "z-50")}
|
||||
>
|
||||
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||
<div className="fixed inset-0 bg-black/30 z-40" aria-hidden={true} />
|
||||
|
||||
{/* Full-screen container to center the panel */}
|
||||
<div
|
||||
className={cn(
|
||||
`fixed inset-0 flex items-center justify-center p-4 z-40`,
|
||||
)}
|
||||
>
|
||||
{/* The actual dialog panel */}
|
||||
<Dialog.Panel
|
||||
className={cn(
|
||||
`mx-auto rounded-lg bg-white w-full shadow-modal max-h-full overflow-y-auto flex flex-col`,
|
||||
|
||||
size === "md"
|
||||
? `max-w-md`
|
||||
: size === "lg"
|
||||
? `max-w-lg`
|
||||
: `max-w-sm`,
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<Dialog.Title className="text-lg border-b h-14 flex items-center px-5 space-x-2 py-4 relative">
|
||||
{titleIcon && <span>{titleIcon}</span>}
|
||||
<span className="truncate flex items-center w-full">{title}</span>
|
||||
<span
|
||||
className="absolute right-1 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<i className="i-mingcute:close-line inline-block" />
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
)}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
({ open, setOpen, children, title, titleIcon, size = "md", zIndex }, ref) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
className={clsx("relative", zIndex ? `z-${zIndex}` : "z-50")}
|
||||
>
|
||||
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||
<div className="fixed inset-0 bg-black/30 z-40" aria-hidden={true} />
|
||||
|
||||
{/* Full-screen container to center the panel */}
|
||||
<div
|
||||
className={cn(
|
||||
`fixed inset-0 flex items-center justify-center p-4 z-40`,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{/* The actual dialog panel */}
|
||||
<Dialog.Panel
|
||||
className={cn(
|
||||
`mx-auto rounded-lg bg-white w-full shadow-modal max-h-full overflow-y-auto flex flex-col`,
|
||||
|
||||
size === "md"
|
||||
? `max-w-md`
|
||||
: size === "lg"
|
||||
? `max-w-lg`
|
||||
: `max-w-sm`,
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<Dialog.Title className="text-lg border-b h-14 flex items-center px-5 space-x-2 py-4 relative">
|
||||
{titleIcon && <span>{titleIcon}</span>}
|
||||
<span className="truncate flex items-center w-full">
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
className="absolute right-1 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<i className="i-mingcute:close-line inline-block" />
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
)}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Modal.displayName = "Modal"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { useEffect, useState } from "react"
|
||||
|
||||
export const useIsClient = () => {
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
}, [])
|
||||
return isClient
|
||||
}
|
||||
Loading…
Reference in New Issue