feat: introduce `ModalStack` component for declarative modal creation (#741)
This commit is contained in:
parent
b1566fc059
commit
48aa67e784
|
|
@ -7,6 +7,7 @@ import dynamic from "next/dynamic"
|
|||
import { useParams, useRouter, useSearchParams } from "next/navigation"
|
||||
import {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
|
|
@ -30,7 +31,7 @@ import { EditorToolbar } from "~/components/ui/EditorToolbar"
|
|||
import { FieldLabel } from "~/components/ui/FieldLabel"
|
||||
import { ImageUploader } from "~/components/ui/ImageUploader"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { ModalContentProps, useModalStack } from "~/components/ui/ModalStack"
|
||||
import { Switch } from "~/components/ui/Switch"
|
||||
import { TagInput } from "~/components/ui/TagInput"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
|
|
@ -40,7 +41,7 @@ import {
|
|||
Values,
|
||||
initialEditorState,
|
||||
useEditorState,
|
||||
} from "~/hooks/useEdtiorState"
|
||||
} from "~/hooks/useEditorState"
|
||||
import { useGetState } from "~/hooks/useGetState"
|
||||
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
|
||||
import { useBeforeMounted } from "~/hooks/useSyncOnce"
|
||||
|
|
@ -258,7 +259,28 @@ export default function SubdomainEditor() {
|
|||
}
|
||||
}, [deleteP.isSuccess])
|
||||
|
||||
const [isCheersOpen, setIsCheersOpen] = useState(false)
|
||||
const { present, dismiss } = useModalStack()
|
||||
|
||||
const postUrl = `${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`
|
||||
const transactionUrl = `${CSB_SCAN}/tx/${
|
||||
page.data?.updatedTransactionHash || page.data?.transactionHash
|
||||
}`
|
||||
|
||||
const twitterShareUrl =
|
||||
page.data && site.data
|
||||
? getTwitterShareUrl({
|
||||
page: page.data,
|
||||
site: site.data,
|
||||
t,
|
||||
})
|
||||
: ""
|
||||
|
||||
const getPostUrl = useGetState(postUrl)
|
||||
const getTransactionUrl = useGetState(transactionUrl)
|
||||
const getTwitterShareUrl_ = useGetState(twitterShareUrl)
|
||||
|
||||
useEffect(() => {
|
||||
if (createPage.isSuccess || updatePage.isSuccess) {
|
||||
|
|
@ -283,8 +305,20 @@ export default function SubdomainEditor() {
|
|||
}&type=${searchParams?.get("type")}`,
|
||||
)
|
||||
}
|
||||
const modalId = "publish-modal"
|
||||
present({
|
||||
title: `🎉 ${t("Published!")}`,
|
||||
id: modalId,
|
||||
content: (props) => (
|
||||
<PublishedModal
|
||||
postUrl={getPostUrl()}
|
||||
transactionUrl={getTransactionUrl()}
|
||||
twitterShareUrl={getTwitterShareUrl_()}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
setIsCheersOpen(true)
|
||||
showConfetti()
|
||||
|
||||
createPage.reset()
|
||||
|
|
@ -485,9 +519,7 @@ export default function SubdomainEditor() {
|
|||
)
|
||||
}, [draftKey, subdomain, site.data?.characterId])
|
||||
|
||||
const [isAdvancedOptionsOpen, setIsAdvancedOptionsOpen] =
|
||||
useState<boolean>(false)
|
||||
|
||||
const presentAdvancedModal = useEditorAdvancedModal({ isPost })
|
||||
const extraProperties = (
|
||||
<EditorExtraProperties
|
||||
defaultSlug={defaultSlug}
|
||||
|
|
@ -495,7 +527,7 @@ export default function SubdomainEditor() {
|
|||
isPost={isPost}
|
||||
subdomain={subdomain}
|
||||
userTags={userTags.data?.list || []}
|
||||
openAdvancedOptions={() => setIsAdvancedOptionsOpen(true)}
|
||||
openAdvancedOptions={presentAdvancedModal}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -681,75 +713,13 @@ export default function SubdomainEditor() {
|
|||
isPost={isPost}
|
||||
userTags={userTags.data?.list || []}
|
||||
subdomain={subdomain}
|
||||
openAdvancedOptions={() => setIsAdvancedOptionsOpen(true)}
|
||||
openAdvancedOptions={presentAdvancedModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DashboardMain>
|
||||
<EditorAdvancedOptions
|
||||
updateValue={updateValue}
|
||||
isAdvancedOptionsOpen={isAdvancedOptionsOpen}
|
||||
setIsAdvancedOptionsOpen={setIsAdvancedOptionsOpen}
|
||||
isPost={isPost}
|
||||
/>
|
||||
<Modal
|
||||
open={isCheersOpen}
|
||||
setOpen={setIsCheersOpen}
|
||||
title={`🎉 ${t("Published!")}`}
|
||||
>
|
||||
<div className="p-5">
|
||||
{t(
|
||||
"Your post has been securely stored on the blockchain. Now you may want to",
|
||||
)}
|
||||
<ul className="list-disc pl-5 mt-2 space-y-1">
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
>
|
||||
{t("View the post")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${
|
||||
page.data?.updatedTransactionHash ||
|
||||
page.data?.transactionHash
|
||||
}`}
|
||||
>
|
||||
{t("View the transaction")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={
|
||||
page.data && site.data
|
||||
? getTwitterShareUrl({
|
||||
page: page.data,
|
||||
site: site.data,
|
||||
t,
|
||||
})
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{t("Share to Twitter")}
|
||||
</UniLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button isBlock onClick={() => setIsCheersOpen(false)}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -896,93 +866,139 @@ const EditorExtraProperties = memo(
|
|||
|
||||
EditorExtraProperties.displayName = "EditorExtraProperties"
|
||||
|
||||
const EditorAdvancedOptions = memo(
|
||||
({
|
||||
updateValue,
|
||||
isAdvancedOptionsOpen,
|
||||
setIsAdvancedOptionsOpen,
|
||||
isPost,
|
||||
}: {
|
||||
updateValue: <K extends keyof Values>(key: K, value: Values[K]) => void
|
||||
const useEditorAdvancedModal = ({ isPost }: { isPost: boolean }) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
isAdvancedOptionsOpen: boolean
|
||||
setIsAdvancedOptionsOpen: (state: boolean) => void
|
||||
isPost: boolean
|
||||
}) => {
|
||||
const values = useEditorState(
|
||||
(state) => pick(state, ["disableAISummary", "publishedAt"]),
|
||||
shallow,
|
||||
)
|
||||
const { t } = useTranslation("dashboard")
|
||||
const { present } = useModalStack()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isAdvancedOptionsOpen}
|
||||
setOpen={setIsAdvancedOptionsOpen}
|
||||
title={t("Advanced Settings")}
|
||||
>
|
||||
<div className="p-5 space-y-5">
|
||||
<div>
|
||||
<label className="form-label">
|
||||
{t("Disable AI-generated summary")}
|
||||
</label>
|
||||
<Switch
|
||||
label=""
|
||||
checked={values.disableAISummary}
|
||||
setChecked={(state) => updateValue("disableAISummary", state)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="publishAt">
|
||||
{t("Publish at")}
|
||||
</label>
|
||||
<DateInput
|
||||
className=""
|
||||
allowDeselect
|
||||
clearable
|
||||
valueFormat="YYYY-MM-DD, h:mm a"
|
||||
name="publishAt"
|
||||
id="publishAt"
|
||||
value={
|
||||
values.publishedAt ? new Date(values.publishedAt) : undefined
|
||||
}
|
||||
onChange={(value: Date | null) => {
|
||||
if (value) {
|
||||
updateValue("publishedAt", value.toISOString())
|
||||
} else {
|
||||
updateValue("publishedAt", "")
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: "0.5rem",
|
||||
borderColor: "var(--border-color)",
|
||||
height: "2.5rem",
|
||||
"&:focus-within": {
|
||||
borderColor: "var(--theme-color)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t(
|
||||
`This ${
|
||||
isPost ? "post" : "page"
|
||||
} will be accessible from this time. Leave blank to use the current time.`,
|
||||
)}
|
||||
</div>
|
||||
{values.publishedAt > new Date().toISOString() && (
|
||||
<div className="text-xs mt-1 text-orange-500">
|
||||
{t(
|
||||
"The post is currently not public as its publication date has been scheduled for a future time.",
|
||||
)}
|
||||
</div>
|
||||
return () => {
|
||||
present({
|
||||
title: t("Advanced Settings"),
|
||||
content: () => <EditorAdvancedModal isPost={isPost} />,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const EditorAdvancedModal: FC<{
|
||||
isPost: boolean
|
||||
}> = ({ isPost }) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const values = useEditorState(
|
||||
(state) => pick(state, ["disableAISummary", "publishedAt"]),
|
||||
shallow,
|
||||
)
|
||||
const updateValue = useEditorState.setState
|
||||
return (
|
||||
<div className="p-5 space-y-5">
|
||||
<div>
|
||||
<label className="form-label">
|
||||
{t("Disable AI-generated summary")}
|
||||
</label>
|
||||
<Switch
|
||||
label=""
|
||||
checked={values.disableAISummary}
|
||||
// setChecked={(state) => updateValue("disableAISummary", state)}
|
||||
setChecked={(state) => {
|
||||
updateValue({
|
||||
disableAISummary: state,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="publishAt">
|
||||
{t("Publish at")}
|
||||
</label>
|
||||
<DateInput
|
||||
className="[&_input]:bg-slate-50 [&_input]:text-black/90"
|
||||
allowDeselect
|
||||
clearable
|
||||
valueFormat="YYYY-MM-DD, h:mm a"
|
||||
name="publishAt"
|
||||
id="publishAt"
|
||||
value={values.publishedAt ? new Date(values.publishedAt) : undefined}
|
||||
onChange={(value: Date | null) => {
|
||||
if (value) {
|
||||
updateValue({
|
||||
publishedAt: value.toISOString(),
|
||||
})
|
||||
} else {
|
||||
updateValue({
|
||||
publishedAt: "",
|
||||
})
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: "0.5rem",
|
||||
borderColor: "var(--border-color)",
|
||||
height: "2.5rem",
|
||||
"&:focus-within": {
|
||||
borderColor: "var(--theme-color)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t(
|
||||
`This ${
|
||||
isPost ? "post" : "page"
|
||||
} will be accessible from this time. Leave blank to use the current time.`,
|
||||
)}
|
||||
</div>
|
||||
{values.publishedAt > new Date().toISOString() && (
|
||||
<div className="text-xs mt-1 text-orange-500">
|
||||
{t(
|
||||
"The post is currently not public as its publication date has been scheduled for a future time.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
},
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
EditorAdvancedOptions.displayName = "EditorAdvancedOptions"
|
||||
const PublishedModal = ({
|
||||
dismiss,
|
||||
postUrl,
|
||||
transactionUrl,
|
||||
twitterShareUrl,
|
||||
}: ModalContentProps<{
|
||||
postUrl: string
|
||||
transactionUrl: string
|
||||
twitterShareUrl: string
|
||||
}>) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
{t(
|
||||
"Your post has been securely stored on the blockchain. Now you may want to",
|
||||
)}
|
||||
<ul className="list-disc pl-5 mt-2 space-y-1">
|
||||
<li>
|
||||
<UniLink className="text-accent" href={postUrl}>
|
||||
{t("View the post")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink className="text-accent" href={transactionUrl}>
|
||||
{t("View the transaction")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink className="text-accent" href={twitterShareUrl}>
|
||||
{t("Share to Twitter")}
|
||||
</UniLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button isBlock onClick={() => dismiss()}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { QueryClient } from "@tanstack/react-query"
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"
|
||||
|
||||
import { ModalStackProvider } from "~/components/ui/ModalStack"
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { useDarkMode } from "~/hooks/useDarkMode"
|
||||
import { useMobileLayout } from "~/hooks/useMobileLayout"
|
||||
|
|
@ -84,7 +85,10 @@ export default function Providers({
|
|||
signInStrategy="simple"
|
||||
ignoreWalletDisconnectEvent={true}
|
||||
>
|
||||
<LangProvider lang={lang}>{children}</LangProvider>
|
||||
<LangProvider lang={lang}>
|
||||
<ModalStackProvider>{children}</ModalStackProvider>
|
||||
</LangProvider>
|
||||
|
||||
<NotificationModal
|
||||
colorScheme={colorScheme}
|
||||
filter={filterNotificationCharacter}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
import { usePatronModal } from "~/components/common/PatronModal"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
|
|
@ -20,17 +18,17 @@ export const PatronButton = ({
|
|||
loadingStatusChange?: (status: boolean) => void
|
||||
}) => {
|
||||
const { t } = useTranslation("common")
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const presentPatronModal = usePatronModal()
|
||||
|
||||
return (
|
||||
<>
|
||||
<PatronModal open={open} setOpen={setOpen} site={site} />
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label={"Patron"}
|
||||
key={t("Patron")}
|
||||
className={cn(className, "-mx-2 text-red-400")}
|
||||
onClick={() => setOpen(true)}
|
||||
onClick={() => presentPatronModal(site)}
|
||||
>
|
||||
<span className="inline-flex items-center">
|
||||
<i className="text-red-400 text-xl inline-flex items-center">
|
||||
|
|
|
|||
|
|
@ -7,28 +7,54 @@ import { useConnectModal } from "@crossbell/connect-kit"
|
|||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { BoxRadio } from "~/components/ui/BoxRadio"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { CSB_SCAN, MIRA_LINK } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { useGetTips, useTipCharacter } from "~/queries/site"
|
||||
|
||||
import { ModalContentProps, useModalStack } from "../ui/ModalStack"
|
||||
import { Tabs } from "../ui/Tabs"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
import { CharacterFloatCard } from "./CharacterFloatCard"
|
||||
|
||||
export const PatronModal = ({
|
||||
export const usePatronModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const { t } = useTranslation("common")
|
||||
return (site?: ExpandedCharacter, page?: ExpandedNote) => {
|
||||
const title =
|
||||
(page
|
||||
? t("Tip the post: {{name}}", {
|
||||
name: page.metadata?.content?.title,
|
||||
})
|
||||
: t("Become a patron of {{name}}", {
|
||||
name: site?.metadata?.content?.name,
|
||||
})) || ""
|
||||
|
||||
present({
|
||||
title: (
|
||||
<span className="inline-flex items-center justify-center w-full space-x-1">
|
||||
<span className="text-red-500 flex w-6 h-6 -mb-[1px]">
|
||||
<i className="icon-[mingcute--heart-fill] text-2xl -mb-[1px]" />
|
||||
</span>
|
||||
<span className="truncate">{title}</span>
|
||||
</span>
|
||||
),
|
||||
content: (props) => <PatronModal {...props} page={page} site={site} />,
|
||||
modalProps: {
|
||||
size: "lg",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
const PatronModal = ({
|
||||
site,
|
||||
page,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
dismiss,
|
||||
}: ModalContentProps<{
|
||||
site?: ExpandedCharacter
|
||||
page?: ExpandedNote
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}) => {
|
||||
}>) => {
|
||||
const { t } = useTranslation("common")
|
||||
const tipCharacter = useTipCharacter()
|
||||
const tips = useGetTips(
|
||||
|
|
@ -79,7 +105,7 @@ export const PatronModal = ({
|
|||
noteId: page?.noteId,
|
||||
})
|
||||
} else {
|
||||
setOpen(false)
|
||||
dismiss()
|
||||
connectModal.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -124,19 +150,7 @@ export const PatronModal = ({
|
|||
})) || ""
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title={
|
||||
<span className="inline-flex items-center justify-center w-full space-x-1">
|
||||
<span className="text-red-500 flex w-6 h-6 -mb-[1px]">
|
||||
<i className="icon-[mingcute--heart-fill] text-2xl -mb-[1px]" />
|
||||
</span>
|
||||
<span className="truncate">{title}</span>
|
||||
</span>
|
||||
}
|
||||
size="lg"
|
||||
>
|
||||
<>
|
||||
<div className="px-5 py-4 space-y-4 text-center">
|
||||
<div className="space-y-1">
|
||||
<span className="flex items-center justify-center">
|
||||
|
|
@ -263,6 +277,6 @@ export const PatronModal = ({
|
|||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import confetti from "canvas-confetti"
|
|||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { CharacterList } from "~/components/common/CharacterList"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
|
|
@ -19,6 +18,7 @@ import {
|
|||
|
||||
import { AvatarStack } from "../ui/AvatarStack"
|
||||
import { Button } from "../ui/Button"
|
||||
import { ModalContentProps, useModalStack } from "../ui/ModalStack"
|
||||
|
||||
export const ReactionLike = ({
|
||||
size,
|
||||
|
|
@ -32,9 +32,8 @@ export const ReactionLike = ({
|
|||
vertical?: boolean
|
||||
}) => {
|
||||
const toggleLikePage = useToggleLikePage()
|
||||
const { t, i18n } = useTranslation("common")
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const [isLikeOpen, setIsLikeOpen] = useState(false)
|
||||
const [isLikeListOpen, setIsLikeListOpen] = useState(false)
|
||||
const likeRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
|
|
@ -51,12 +50,37 @@ export const ReactionLike = ({
|
|||
noteId,
|
||||
})
|
||||
|
||||
const [isUnlikeOpen, setIsUnlikeOpen] = useState(false)
|
||||
const { present } = useModalStack()
|
||||
|
||||
const presentUnlikeModal = () => {
|
||||
if (characterId && noteId) {
|
||||
present({
|
||||
title: t("Confirm to revert"),
|
||||
content: (props) => (
|
||||
<UnLikeModal
|
||||
{...props}
|
||||
characterId={characterId}
|
||||
noteId={noteId}
|
||||
likeStatus={likeStatus}
|
||||
toggleLikePage={toggleLikePage}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
const like = () => {
|
||||
if (characterId && noteId) {
|
||||
if (likeStatus.isLiked) {
|
||||
setIsLikeOpen(true)
|
||||
present({
|
||||
title: t("Like successfully") || "",
|
||||
content: (props) => (
|
||||
<LikeModal
|
||||
{...props}
|
||||
likeStatus={likeStatus}
|
||||
presentUnlikeModal={presentUnlikeModal}
|
||||
/>
|
||||
),
|
||||
})
|
||||
} else {
|
||||
toggleLikePage.mutate({
|
||||
characterId,
|
||||
|
|
@ -69,7 +93,7 @@ export const ReactionLike = ({
|
|||
|
||||
const unlike = () => {
|
||||
if (characterId && noteId) {
|
||||
setIsUnlikeOpen(false)
|
||||
presentUnlikeModal()
|
||||
if (likeStatus.isLiked) {
|
||||
toggleLikePage.mutate({
|
||||
noteId,
|
||||
|
|
@ -167,57 +191,7 @@ export const ReactionLike = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
open={isLikeOpen}
|
||||
setOpen={setIsLikeOpen}
|
||||
title={t("Like successfully") || ""}
|
||||
>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="like stored" i18n={i18n}>
|
||||
Your like has been stored on the blockchain, view it on{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${likeStatus.transactionHash}`}
|
||||
>
|
||||
Crossbell Scan
|
||||
</UniLink>
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="border-t flex flex-col md:flex-row gap-4 items-center px-5 py-4">
|
||||
<Button isBlock onClick={() => setIsLikeOpen(false)}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isBlock
|
||||
onClick={() => {
|
||||
setIsUnlikeOpen(true)
|
||||
setIsLikeOpen(false)
|
||||
}}
|
||||
>
|
||||
{t("Revert")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={isUnlikeOpen}
|
||||
setOpen={setIsUnlikeOpen}
|
||||
title={t("Confirm to revert")}
|
||||
>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="like revert" i18n={i18n}>
|
||||
Do you really want to revert this like action?
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="border-t flex flex-col md:flex-row gap-4 items-center px-5 py-4">
|
||||
<Button isBlock onClick={() => setIsUnlikeOpen(false)}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button variant="secondary" isBlock onClick={() => unlike()}>
|
||||
{t("Confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{showAvatarStack && (
|
||||
<CharacterList
|
||||
open={isLikeListOpen}
|
||||
|
|
@ -231,3 +205,94 @@ export const ReactionLike = ({
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const LikeModal = ({
|
||||
dismiss,
|
||||
likeStatus,
|
||||
|
||||
presentUnlikeModal,
|
||||
}: ModalContentProps<{
|
||||
likeStatus: any
|
||||
|
||||
presentUnlikeModal: () => void
|
||||
}>) => {
|
||||
const { t, i18n } = useTranslation("common")
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="like stored" i18n={i18n}>
|
||||
Your like has been stored on the blockchain, view it on{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${likeStatus.transactionHash}`}
|
||||
>
|
||||
Crossbell Scan
|
||||
</UniLink>
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="border-t flex flex-col md:flex-row gap-4 items-center px-5 py-4">
|
||||
<Button isBlock onClick={dismiss}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isBlock
|
||||
onClick={() => {
|
||||
presentUnlikeModal()
|
||||
dismiss()
|
||||
}}
|
||||
>
|
||||
{t("Revert")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const UnLikeModal = ({
|
||||
dismiss,
|
||||
characterId,
|
||||
noteId,
|
||||
likeStatus,
|
||||
toggleLikePage,
|
||||
}: ModalContentProps<{
|
||||
characterId: number
|
||||
noteId: number
|
||||
likeStatus: any
|
||||
toggleLikePage: ReturnType<typeof useToggleLikePage>
|
||||
}>) => {
|
||||
const { t, i18n } = useTranslation("common")
|
||||
|
||||
const { present } = useModalStack()
|
||||
const unlike = () => {
|
||||
if (characterId && noteId) {
|
||||
dismiss()
|
||||
if (likeStatus.isLiked) {
|
||||
toggleLikePage.mutate({
|
||||
noteId,
|
||||
characterId,
|
||||
action: "unlink",
|
||||
})
|
||||
} // else do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="like revert" i18n={i18n}>
|
||||
Do you really want to revert this like action?
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="border-t flex flex-col md:flex-row gap-4 items-center px-5 py-4">
|
||||
<Button isBlock onClick={dismiss}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button variant="secondary" isBlock onClick={() => unlike()}>
|
||||
{t("Confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
"use client"
|
||||
|
||||
import confetti from "canvas-confetti"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { FC, useEffect, useMemo, useRef, useState } from "react"
|
||||
|
||||
import { useAccountState } from "@crossbell/connect-kit"
|
||||
|
||||
import { CharacterList } from "~/components/common/CharacterList"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { CSB_SCAN, CSB_XCHAR } from "~/lib/env"
|
||||
|
|
@ -17,6 +16,7 @@ import { useCheckMint, useGetMints, useMintPage } from "~/queries/page"
|
|||
|
||||
import { AvatarStack } from "../ui/AvatarStack"
|
||||
import { Button } from "../ui/Button"
|
||||
import { ModalContentProps, useModalStack } from "../ui/ModalStack"
|
||||
|
||||
export const ReactionMint = ({
|
||||
size,
|
||||
|
|
@ -34,7 +34,6 @@ export const ReactionMint = ({
|
|||
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
|
||||
const [isMintOpen, setIsMintOpen] = useState(false)
|
||||
const [isMintListOpen, setIsMintListOpen] = useState(false)
|
||||
const mintRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
|
|
@ -47,11 +46,14 @@ export const ReactionMint = ({
|
|||
characterId,
|
||||
noteId,
|
||||
})
|
||||
|
||||
const presentMintModal = usePresentMintModal({
|
||||
handle: account?.character?.handle || "",
|
||||
transactionHash: isMint.data?.list?.[0]?.transactionHash || "",
|
||||
})
|
||||
const mint = () => {
|
||||
if (characterId && noteId) {
|
||||
if (isMint.data?.count) {
|
||||
setIsMintOpen(true)
|
||||
presentMintModal()
|
||||
} else {
|
||||
mintPage.mutate({
|
||||
characterId,
|
||||
|
|
@ -158,35 +160,7 @@ export const ReactionMint = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
open={isMintOpen}
|
||||
setOpen={setIsMintOpen}
|
||||
title={t("Mint successfully") || ""}
|
||||
>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="mint stored" i18n={i18n}>
|
||||
This post has been minted to NFT by you, view it on{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_XCHAR}/${account?.character?.handle}/collections`}
|
||||
>
|
||||
xChar
|
||||
</UniLink>{" "}
|
||||
or{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${isMint.data?.list?.[0]?.transactionHash}`}
|
||||
>
|
||||
Crossbell Scan
|
||||
</UniLink>
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button isBlock onClick={() => setIsMintOpen(false)}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{showAvatarStack && (
|
||||
<CharacterList
|
||||
open={isMintListOpen}
|
||||
|
|
@ -200,3 +174,55 @@ export const ReactionMint = ({
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const usePresentMintModal = (props: MintModalProps) => {
|
||||
const { present } = useModalStack()
|
||||
const { t, i18n } = useTranslation("common")
|
||||
return () => {
|
||||
present({
|
||||
title: t("Mint successfully") || "",
|
||||
content: (rest) => <MintModal {...rest} {...props} />,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
interface MintModalProps {
|
||||
handle: string
|
||||
transactionHash: string
|
||||
}
|
||||
|
||||
const MintModal: FC<ModalContentProps<MintModalProps>> = ({
|
||||
handle,
|
||||
dismiss,
|
||||
transactionHash,
|
||||
}) => {
|
||||
const { t, i18n } = useTranslation("common")
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-5">
|
||||
<Trans i18nKey="mint stored" i18n={i18n}>
|
||||
This post has been minted to NFT by you, view it on{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_XCHAR}/${handle}/collections`}
|
||||
>
|
||||
xChar
|
||||
</UniLink>{" "}
|
||||
or{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${transactionHash}`}
|
||||
>
|
||||
Crossbell Scan
|
||||
</UniLink>
|
||||
</Trans>
|
||||
</div>
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button isBlock onClick={dismiss}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import dynamic from "next/dynamic"
|
||||
import { FC, useState } from "react"
|
||||
import { FC } from "react"
|
||||
import { toast } from "react-hot-toast"
|
||||
|
||||
import { useIsClient } from "~/hooks/useClient"
|
||||
|
|
@ -7,7 +7,7 @@ import { useTranslation } from "~/lib/i18n/client"
|
|||
import { cn } from "~/lib/utils"
|
||||
|
||||
import { Button } from "../ui/Button"
|
||||
import { Modal } from "../ui/Modal"
|
||||
import { useModalStack } from "../ui/ModalStack"
|
||||
import { Tooltip } from "../ui/Tooltip"
|
||||
|
||||
const QRCodeSVG = dynamic(
|
||||
|
|
@ -59,19 +59,26 @@ export const ReactionShare: FC<{
|
|||
size?: "sm" | "base"
|
||||
}> = ({ vertical, size }) => {
|
||||
const { t } = useTranslation("common")
|
||||
const [isShareOpen, setIsShareOpen] = useState(false)
|
||||
|
||||
const isClient = useIsClient()
|
||||
|
||||
const presentModal = usePresentShareModal()
|
||||
|
||||
if (!isClient) return null
|
||||
|
||||
// TODO: get from Note
|
||||
|
||||
const handleShare = () => {
|
||||
setIsShareOpen(true)
|
||||
const title = document.title
|
||||
const url = location.href
|
||||
const text = t("Share Message", { title })
|
||||
presentModal({
|
||||
url,
|
||||
title,
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: get from Note
|
||||
const title = document.title
|
||||
const url = location.href
|
||||
const text = t("Share Message", { title })
|
||||
return (
|
||||
<>
|
||||
<div className={cn("xlog-reactions-share flex items-center sm:mb-0")}>
|
||||
|
|
@ -112,38 +119,52 @@ export const ReactionShare: FC<{
|
|||
})()}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal
|
||||
open={isShareOpen}
|
||||
setOpen={setIsShareOpen}
|
||||
title={t("Share Modal") || ""}
|
||||
>
|
||||
<div className="relative grid grid-cols-[200px_auto] gap-5 px-5 py-6">
|
||||
<div className="qrcode inline-block min-h-[200px]">
|
||||
<QRCodeSVG
|
||||
value={url}
|
||||
className="aspect-square w-[200px]"
|
||||
height={200}
|
||||
width={200}
|
||||
/>
|
||||
</div>
|
||||
<div className="share-options flex flex-col gap-2">
|
||||
<ul className="w-[200px] flex-col gap-2 [&>li]:flex [&>li]:items-center [&>li]:space-x-2">
|
||||
{shareList.map(({ name, icon, onClick }) => (
|
||||
<li
|
||||
key={name}
|
||||
className="flex cursor-pointer items-center space-x-2 rounded-md px-3 py-2 text-lg transition-colors hover:bg-gray-100 [&_img]:w-[1rem] [&_img]:h-[1rem]"
|
||||
aria-label={`Share to ${name}`}
|
||||
role="button"
|
||||
onClick={() => onClick({ url, text, title })}
|
||||
>
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const usePresentShareModal = () => {
|
||||
const { present } = useModalStack()
|
||||
const { t } = useTranslation("common")
|
||||
return (props: ShareModalProps) => {
|
||||
present({
|
||||
title: t("Share Modal") || "",
|
||||
content: () => <ShareModal {...props} />,
|
||||
})
|
||||
}
|
||||
}
|
||||
interface ShareModalProps {
|
||||
url: string
|
||||
title: string
|
||||
text: string
|
||||
}
|
||||
const ShareModal: FC<ShareModalProps> = ({ url, text, title }) => {
|
||||
return (
|
||||
<div className="relative grid grid-cols-[200px_auto] gap-5 px-5 py-6">
|
||||
<div className="qrcode inline-block min-h-[200px]">
|
||||
<QRCodeSVG
|
||||
value={url}
|
||||
className="aspect-square w-[200px]"
|
||||
height={200}
|
||||
width={200}
|
||||
/>
|
||||
</div>
|
||||
<div className="share-options flex flex-col gap-2">
|
||||
<ul className="w-[200px] flex-col gap-2 [&>li]:flex [&>li]:items-center [&>li]:space-x-2">
|
||||
{shareList.map(({ name, icon, onClick }) => (
|
||||
<li
|
||||
key={name}
|
||||
className="flex cursor-pointer items-center space-x-2 rounded-md px-3 py-2 text-lg transition-colors hover:bg-gray-100 [&_img]:w-[1rem] [&_img]:h-[1rem]"
|
||||
aria-label={`Share to ${name}`}
|
||||
role="button"
|
||||
onClick={() => onClick({ url, text, title })}
|
||||
>
|
||||
{icon}
|
||||
<span>{name}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useRef, useState } from "react"
|
||||
import { useMemo, useRef } from "react"
|
||||
|
||||
import { useAccountState } from "@crossbell/connect-kit"
|
||||
|
||||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
import { usePatronModal } from "~/components/common/PatronModal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { noopArr } from "~/lib/noop"
|
||||
|
|
@ -32,7 +32,6 @@ export const ReactionTip = ({
|
|||
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
|
||||
const [isTipOpen, setIsTipOpen] = useState(false)
|
||||
const tipRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const tips = useGetTips({
|
||||
|
|
@ -45,9 +44,11 @@ export const ReactionTip = ({
|
|||
characterId: account?.characterId || "0",
|
||||
})
|
||||
|
||||
const presentPatronModal = usePatronModal()
|
||||
|
||||
const tip = () => {
|
||||
if (characterId && noteId) {
|
||||
setIsTipOpen(true)
|
||||
presentPatronModal(site, page)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,12 +112,6 @@ export const ReactionTip = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<PatronModal
|
||||
site={site}
|
||||
open={isTipOpen}
|
||||
setOpen={setIsTipOpen}
|
||||
page={page}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Dialog, Transition } from "@headlessui/react"
|
|||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
interface ModalProps {
|
||||
export interface ModalProps {
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
children: React.ReactNode
|
||||
|
|
@ -16,6 +16,7 @@ interface ModalProps {
|
|||
zIndex?: number
|
||||
panelClassName?: string
|
||||
boxClassName?: string
|
||||
afterLeave?: () => void
|
||||
}
|
||||
|
||||
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
||||
|
|
@ -30,11 +31,12 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
zIndex,
|
||||
panelClassName,
|
||||
boxClassName,
|
||||
afterLeave,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<Transition appear show={open} as={Fragment}>
|
||||
<Transition appear show={open} as={Fragment} afterLeave={afterLeave}>
|
||||
<Dialog
|
||||
onClose={() => setOpen(false)}
|
||||
className="relative"
|
||||
|
|
@ -76,7 +78,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
{/* 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`,
|
||||
`mx-auto rounded-lg bg-white w-full shadow-modal max-h-full flex flex-col`,
|
||||
size === "md"
|
||||
? `max-w-md`
|
||||
: size === "lg"
|
||||
|
|
@ -92,7 +94,7 @@ export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|||
{title}
|
||||
</span>
|
||||
<span
|
||||
className="absolute right-1 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
|
||||
className="absolute right-4 w-7 h-7 text-xl cursor-pointer bg-white flex items-center justify-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<i className="icon-[mingcute--close-line] inline-block" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
/* eslint-disable react/prop-types */
|
||||
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import type { FC, PropsWithChildren, ReactNode, SetStateAction } from "react"
|
||||
import {
|
||||
Dispatch,
|
||||
createContext,
|
||||
createElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
|
||||
import {
|
||||
Modal as ModalImpl,
|
||||
ModalProps as ModalImplProps,
|
||||
} from "~/components/ui/Modal"
|
||||
import { useIsClient } from "~/hooks/useClient"
|
||||
|
||||
const modalIdToPropsMap = {} as Record<string, ModalProps>
|
||||
|
||||
export type ModalContentProps<T = {}> = { dismiss: () => void } & T
|
||||
interface ModalProps {
|
||||
title: ReactNode
|
||||
content: FC<ModalContentProps>
|
||||
|
||||
modalProps?: Partial<Omit<ModalImplProps, "open" | "setOpen" | "afterLeave">>
|
||||
}
|
||||
|
||||
interface ModalInstance {
|
||||
modalClose: () => void
|
||||
}
|
||||
interface ModalStackContextValue extends ModalProps {
|
||||
id: string
|
||||
ins?: ModalInstance
|
||||
}
|
||||
const ModalStackContext = createContext<ModalStackContextValue[]>([])
|
||||
const SetModalStackContext = createContext<
|
||||
Dispatch<SetStateAction<ModalStackContextValue[]>>
|
||||
>(() => void 0)
|
||||
|
||||
export const useModalStack = () => {
|
||||
const id = useId()
|
||||
const currentCount = useRef(0)
|
||||
const setStack = useContext(SetModalStackContext)
|
||||
return useMemo(
|
||||
() => ({
|
||||
present(props: ModalProps & { id?: string }) {
|
||||
const modalId = `${id}-${currentCount.current++}`
|
||||
setStack((p) => {
|
||||
const modalProps = {
|
||||
...props,
|
||||
id: props.id ?? modalId,
|
||||
}
|
||||
modalIdToPropsMap[modalProps.id] = modalProps
|
||||
return p.concat(modalProps)
|
||||
})
|
||||
|
||||
return () => {
|
||||
setStack((p) => {
|
||||
return p.filter((item) => item.id !== modalId)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
dismiss(id: string) {
|
||||
setStack((p) => {
|
||||
const m = p.find((item) => item.id === id)
|
||||
if (m?.ins) {
|
||||
m.ins.modalClose()
|
||||
return p
|
||||
}
|
||||
|
||||
return p.filter((item) => item.id !== id)
|
||||
})
|
||||
},
|
||||
}),
|
||||
[id, setStack],
|
||||
)
|
||||
}
|
||||
|
||||
export const ModalStackProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [modal, setModal] = useState(
|
||||
[] as React.ContextType<typeof ModalStackContext>,
|
||||
)
|
||||
return (
|
||||
<SetModalStackContext.Provider value={setModal}>
|
||||
{children}
|
||||
<ModalStackContext.Provider value={modal}>
|
||||
<ModalStack />
|
||||
</ModalStackContext.Provider>
|
||||
</SetModalStackContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const ModalStack = () => {
|
||||
const stack = useContext(ModalStackContext)
|
||||
|
||||
const isClient = useIsClient()
|
||||
if (!isClient) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{stack.map((item, index) => {
|
||||
return <Modal key={item.id} item={item} index={index} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const Modal = memo<{
|
||||
item: ModalProps & { id: string }
|
||||
index: number
|
||||
}>(function Modal({ item, index }) {
|
||||
const setStack = useContext(SetModalStackContext)
|
||||
const close = useCallback(() => {
|
||||
setStack((p) => {
|
||||
return p.filter((modal) => modal.id !== item.id)
|
||||
})
|
||||
}, [item.id])
|
||||
|
||||
const instanceRef = useRef<ModalInstance>({
|
||||
modalClose: () => {
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const { content, title, modalProps } = item
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
setOpen(true)
|
||||
|
||||
// set instanceRef
|
||||
|
||||
setStack((p) => {
|
||||
const newStack = [...p]
|
||||
newStack[index].ins = instanceRef.current
|
||||
return newStack
|
||||
})
|
||||
}, [])
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
const afterLeave = useCallback(() => {
|
||||
close()
|
||||
}, [])
|
||||
return (
|
||||
<ModalImpl
|
||||
open={open}
|
||||
afterLeave={afterLeave}
|
||||
setOpen={handleClose}
|
||||
title={title}
|
||||
{...modalProps}
|
||||
>
|
||||
{createElement(content, {
|
||||
dismiss: handleClose,
|
||||
})}
|
||||
</ModalImpl>
|
||||
)
|
||||
})
|
||||
|
|
@ -5,7 +5,7 @@ import { shallow } from "zustand/shallow"
|
|||
import { Combobox, Transition } from "@headlessui/react"
|
||||
import { ChevronUpDownIcon, XMarkIcon } from "@heroicons/react/20/solid"
|
||||
|
||||
import { useEditorState } from "~/hooks/useEdtiorState"
|
||||
import { useEditorState } from "~/hooks/useEditorState"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
import { CustomInputProps } from "./Input"
|
||||
|
|
|
|||
Loading…
Reference in New Issue