feat: note tip
This commit is contained in:
parent
f9ff893763
commit
2145811350
|
|
@ -55,11 +55,14 @@
|
|||
"Patron": "赞助",
|
||||
"Become a patron of {{name}}" : "成为 {{name}} 的赞助者",
|
||||
"Latest patrons": "最近赞助者",
|
||||
"Latest tipper": "最近赞赏者",
|
||||
"You are here to be the first patron.": "你将是第一个赞助者。",
|
||||
"You are here to be the first tipper.": "你将是第一个赞赏者。",
|
||||
"Select a tier": "选择一个等级",
|
||||
"One-time": "一次性",
|
||||
"Monthly and NFT Rewards": "每月和 NFT 奖励",
|
||||
"Coming soon": "即将推出",
|
||||
"What is Mira? Where can I get some?" : "Mira 是什么?我在哪里可以获得?",
|
||||
"Tip": "赞赏"
|
||||
"Tip": "赞赏",
|
||||
"Tip the post: {{name}}": "赞赏文章:{{name}}"
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "~/components/ui/Button"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { HeartIcon } from "@heroicons/react/24/solid"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
|
|
@ -16,16 +16,19 @@ import { CharacterFloatCard } from "./CharacterFloatCard"
|
|||
import { UniLink } from "../ui/UniLink"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { parsePageId } from "~/models/page.model"
|
||||
|
||||
export const PatronModal: React.FC<{
|
||||
site: Profile | undefined | null
|
||||
page?: Note | null
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}> = ({ site, open, setOpen }) => {
|
||||
}> = ({ site, page, open, setOpen }) => {
|
||||
const { t } = useTranslation("common")
|
||||
const tipCharacter = useTipCharacter()
|
||||
const tips = useGetTips({
|
||||
toCharacterId: site?.metadata?.proof,
|
||||
toNoteId: parsePageId(page?.id || "").noteId,
|
||||
})
|
||||
|
||||
const radios = [
|
||||
|
|
@ -66,6 +69,7 @@ export const PatronModal: React.FC<{
|
|||
fromCharacterId: currentCharacterId,
|
||||
toCharacterId: site?.metadata?.proof,
|
||||
amount: parseInt(value),
|
||||
noteId: parsePageId(page?.id || "").noteId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -100,15 +104,20 @@ export const PatronModal: React.FC<{
|
|||
}
|
||||
}, [tipCharacter.isError, t])
|
||||
|
||||
const title =
|
||||
(page
|
||||
? t("Tip the post: {{name}}", {
|
||||
name: page.title,
|
||||
})
|
||||
: t("Become a patron of {{name}}", {
|
||||
name: site?.name,
|
||||
})) || ""
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title={
|
||||
t("Become a patron of {{name}}", {
|
||||
name: site?.name,
|
||||
}) || ""
|
||||
}
|
||||
title={title}
|
||||
titleIcon={<HeartIcon className="text-red-500 flex w-6 h-6 -mb-[1px]" />}
|
||||
size="lg"
|
||||
>
|
||||
|
|
@ -131,12 +140,14 @@ export const PatronModal: React.FC<{
|
|||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg">{t("Latest patrons")}</div>
|
||||
<div className="text-lg">
|
||||
{t(page ? "Latest tipper" : "Latest patrons")}
|
||||
</div>
|
||||
<div className="text-zinc-500 text-sm mt-2">
|
||||
{tips.data?.list?.length ? (
|
||||
{tips.data?.pages?.[0]?.list?.length ? (
|
||||
<>
|
||||
<ul className="grid grid-cols-8">
|
||||
{tips.data.list?.map((tip, index) => (
|
||||
<ul className="grid grid-cols-9">
|
||||
{tips.data.pages[0].list?.map((tip, index) => (
|
||||
<li
|
||||
className="inline-flex flex-col items-center"
|
||||
key={index}
|
||||
|
|
@ -153,7 +164,7 @@ export const PatronModal: React.FC<{
|
|||
tip.character?.metadata?.content?.avatars || []
|
||||
}
|
||||
name={tip.character?.metadata?.content?.name}
|
||||
size={55}
|
||||
size={50}
|
||||
/>
|
||||
</UniLink>
|
||||
</CharacterFloatCard>
|
||||
|
|
@ -167,17 +178,21 @@ export const PatronModal: React.FC<{
|
|||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
{tips.data.count > 10 && (
|
||||
{tips.data.pages?.[0]?.count > 8 && (
|
||||
<li className="inline-block">
|
||||
<div className="relative align-middle border-2 border-white w-10 h-10 rounded-full inline-flex bg-gray-100 items-center justify-center text-gray-400 font-medium">
|
||||
+{tips.data.count - 10}
|
||||
<div className="relative align-middle border-2 border-white w-[55px] h-[55px] rounded-full inline-flex bg-gray-100 items-center justify-center text-gray-400 font-medium">
|
||||
+{tips.data.pages?.[0]?.count - 8}
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
t("You are here to be the first patron.")
|
||||
t(
|
||||
page
|
||||
? "You are here to be the first tipper."
|
||||
: "You are here to be the first patron.",
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -219,9 +234,7 @@ export const PatronModal: React.FC<{
|
|||
isLoading={tipCharacter.isLoading}
|
||||
ref={submitRef}
|
||||
>
|
||||
{t("Become a patron of {{name}}", {
|
||||
name: site?.name,
|
||||
})}
|
||||
<span className="truncate">{title}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
useToggleLikePage,
|
||||
useGetLikeCounts,
|
||||
} from "~/queries/page"
|
||||
import { useAccountSites } from "~/queries/site"
|
||||
import { useAccountSites, useGetTips } from "~/queries/site"
|
||||
import { useState, useEffect, useRef } from "react"
|
||||
import { Button } from "../ui/Button"
|
||||
import { CSB_SCAN, CSB_XCHAR } from "~/lib/env"
|
||||
|
|
@ -21,20 +21,24 @@ import { cn } from "~/lib/utils"
|
|||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import confetti from "canvas-confetti"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { HeartIcon } from "@heroicons/react/24/solid"
|
||||
import { useAccountState } from "@crossbell/connect-kit"
|
||||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
|
||||
export const Reactions: React.FC<{
|
||||
className?: string
|
||||
size?: "sm" | "base"
|
||||
pageId?: string
|
||||
site?: Profile | null
|
||||
}> = ({ className, size, pageId, site }) => {
|
||||
page?: Note | null
|
||||
}> = ({ className, size, pageId, site, page }) => {
|
||||
const toggleLikePage = useToggleLikePage()
|
||||
const mintPage = useMintPage()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
// like
|
||||
const userSite = useAccountSites()
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
const [isLikeOpen, setIsLikeOpen] = useState(false)
|
||||
const [isLikeListOpen, setIsLikeListOpen] = useState(false)
|
||||
const likeRef = useRef<HTMLButtonElement>(null)
|
||||
|
|
@ -134,6 +138,27 @@ export const Reactions: React.FC<{
|
|||
}
|
||||
}, [mintPage.isSuccess])
|
||||
|
||||
// tip
|
||||
const [isTipOpen, setIsTipOpen] = useState(false)
|
||||
const tipRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const { characterId, noteId } = parsePageId(pageId || "")
|
||||
const tips = useGetTips({
|
||||
toCharacterId: characterId,
|
||||
toNoteId: noteId,
|
||||
})
|
||||
const isTip = useGetTips({
|
||||
toCharacterId: characterId,
|
||||
toNoteId: noteId,
|
||||
characterId: account?.characterId,
|
||||
})
|
||||
|
||||
const tip = () => {
|
||||
if (pageId) {
|
||||
setIsTipOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -151,7 +176,7 @@ export const Reactions: React.FC<{
|
|||
}`}
|
||||
isAutoWidth={true}
|
||||
onClick={like}
|
||||
isLoading={userSite.isLoading || toggleLikePage.isPending}
|
||||
isLoading={toggleLikePage.isPending}
|
||||
ref={likeRef}
|
||||
>
|
||||
{(() => {
|
||||
|
|
@ -260,6 +285,58 @@ export const Reactions: React.FC<{
|
|||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{size !== "sm" && (
|
||||
<div className="xlog-reactions-tip flex items-center">
|
||||
<Button
|
||||
variant="tip"
|
||||
className={`flex items-center mr-2 ${
|
||||
isTip.isSuccess && isTip.data.pages?.[0].count && "active"
|
||||
}`}
|
||||
isAutoWidth={true}
|
||||
onClick={tip}
|
||||
ref={tipRef}
|
||||
>
|
||||
{(() => {
|
||||
const inner = <HeartIcon className={cn("w-10 h-10")} />
|
||||
return (
|
||||
<Tooltip label={t("Tip")} placement="top">
|
||||
{inner}
|
||||
</Tooltip>
|
||||
)
|
||||
})()}
|
||||
<span className="ml-2">{tips.data?.pages?.[0]?.count || 0}</span>
|
||||
</Button>
|
||||
{
|
||||
<ul
|
||||
className="-space-x-4 cursor-pointer hidden sm:inline-block"
|
||||
onClick={tip}
|
||||
>
|
||||
{tips.data?.pages?.[0]?.list
|
||||
?.sort((a, b: any) =>
|
||||
b.character?.metadata?.content?.avatars?.[0] ? 1 : -1,
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((tip: any, index) => (
|
||||
<li className="inline-block" key={index}>
|
||||
<Avatar
|
||||
className="relative align-middle border-2 border-white"
|
||||
images={tip.character?.metadata?.content?.avatars || []}
|
||||
name={tip.character?.metadata?.content?.name}
|
||||
size={40}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{(tips.data?.pages?.[0]?.count || 0) > 3 && (
|
||||
<li className="inline-block">
|
||||
<div className="relative align-middle border-2 border-white w-10 h-10 rounded-full inline-flex bg-gray-100 items-center justify-center text-gray-400 font-medium">
|
||||
+{tips.data!.pages?.[0]?.count - 3}
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
open={isLikeOpen}
|
||||
setOpen={setIsLikeOpen}
|
||||
|
|
@ -292,7 +369,7 @@ export const Reactions: React.FC<{
|
|||
This post has been minted to NFT by you, view it on{" "}
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_XCHAR}/${userSite.data?.[0]?.username}/collections`}
|
||||
href={`${CSB_XCHAR}/${account?.character?.handle}/collections`}
|
||||
>
|
||||
xChar
|
||||
</UniLink>{" "}
|
||||
|
|
@ -311,6 +388,12 @@ export const Reactions: React.FC<{
|
|||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<PatronModal
|
||||
site={site}
|
||||
open={isTipOpen}
|
||||
setOpen={setIsTipOpen}
|
||||
page={page}
|
||||
/>
|
||||
<CharacterList
|
||||
open={isLikeListOpen}
|
||||
setOpen={setIsLikeListOpen}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ export const PostFooter: React.FC<{
|
|||
}> = ({ page, site }) => {
|
||||
return (
|
||||
<>
|
||||
<Reactions className="mt-14 mb-12" pageId={page?.id} site={site} />
|
||||
<Reactions
|
||||
className="mt-14 mb-12"
|
||||
pageId={page?.id}
|
||||
site={site}
|
||||
page={page}
|
||||
/>
|
||||
<Comment page={page} />
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export type Variant =
|
|||
| "collect"
|
||||
| "crossbell"
|
||||
| "outline"
|
||||
| "patron"
|
||||
| "tip"
|
||||
|
||||
export type VariantColor = "green" | "red" | "gray" | "gradient" | "black"
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export const Modal: React.FC<{
|
|||
{title && (
|
||||
<Dialog.Title className="text-lg border-b h-14 flex items-center px-5 space-x-2 py-4">
|
||||
{titleIcon && <span>{titleIcon}</span>}
|
||||
<span>{title}</span>
|
||||
<span className="truncate">{title}</span>
|
||||
</Dialog.Title>
|
||||
)}
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -153,15 +153,15 @@ a {
|
|||
fill: #ffcf55;
|
||||
}
|
||||
|
||||
.button.is-patron {
|
||||
.button.is-tip {
|
||||
@apply shadow-none p-0;
|
||||
@apply text-gray-500 fill-gray-500;
|
||||
}
|
||||
.button.is-patron:hover {
|
||||
.button.is-tip:hover {
|
||||
color: #f87171;
|
||||
fill: #f87171;
|
||||
}
|
||||
.button.is-patron.active {
|
||||
.button.is-tip.active {
|
||||
color: #f87171;
|
||||
fill: #f87171;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,27 +520,45 @@ export async function tipCharacter(
|
|||
fromCharacterId: string | number
|
||||
toCharacterId: string | number
|
||||
amount: number
|
||||
noteId?: string | number
|
||||
},
|
||||
contract: Contract,
|
||||
) {
|
||||
const decimals = await contract.getMiraTokenDecimals()
|
||||
return await contract?.tipCharacter(
|
||||
input.fromCharacterId,
|
||||
input.toCharacterId,
|
||||
BigInt(input.amount) * BigInt(10) ** BigInt(decimals?.data || 18),
|
||||
)
|
||||
if (input.noteId) {
|
||||
return await contract?.tipCharacterForNote(
|
||||
input.fromCharacterId,
|
||||
input.toCharacterId,
|
||||
input.noteId,
|
||||
BigInt(input.amount) * BigInt(10) ** BigInt(decimals?.data || 18),
|
||||
)
|
||||
} else {
|
||||
return await contract?.tipCharacter(
|
||||
input.fromCharacterId,
|
||||
input.toCharacterId,
|
||||
BigInt(input.amount) * BigInt(10) ** BigInt(decimals?.data || 18),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTips(
|
||||
input: { toCharacterId: string | number },
|
||||
input: {
|
||||
toCharacterId: string | number
|
||||
characterId?: string | number
|
||||
toNoteId?: string | number
|
||||
cursor?: string
|
||||
},
|
||||
contract: Contract,
|
||||
) {
|
||||
const address = await contract.getMiraTokenAddress()
|
||||
const tips = await indexer?.getTips({
|
||||
characterId: input.characterId,
|
||||
toNoteId: input.toNoteId,
|
||||
toCharacterId: input.toCharacterId,
|
||||
tokenAddress: address?.data || "0xAfB95CC0BD320648B3E8Df6223d9CDD05EbeDC64",
|
||||
includeMetadata: true,
|
||||
limit: 8,
|
||||
cursor: input.cursor,
|
||||
})
|
||||
|
||||
if (tips?.list?.length) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
useQuery,
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
useInfiniteQuery,
|
||||
} from "@tanstack/react-query"
|
||||
import { useContract } from "@crossbell/contract"
|
||||
import {
|
||||
useAccountState,
|
||||
|
|
@ -324,17 +329,29 @@ export function useTipCharacter() {
|
|||
return mutation
|
||||
}
|
||||
|
||||
export const useGetTips = (data: { toCharacterId?: string }) => {
|
||||
export const useGetTips = (
|
||||
data: Partial<Parameters<typeof siteModel.getTips>[0]>,
|
||||
) => {
|
||||
const contract = useContract()
|
||||
return useQuery(["getTips", data], async () => {
|
||||
if (!data.toCharacterId) {
|
||||
return null
|
||||
}
|
||||
return siteModel.getTips(
|
||||
{
|
||||
toCharacterId: data.toCharacterId,
|
||||
},
|
||||
contract,
|
||||
)
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getTips", data],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!data.toCharacterId) {
|
||||
return {
|
||||
count: 0,
|
||||
list: [],
|
||||
cursor: undefined,
|
||||
}
|
||||
}
|
||||
return siteModel.getTips(
|
||||
{
|
||||
...data,
|
||||
toCharacterId: data.toCharacterId,
|
||||
cursor: pageParam,
|
||||
},
|
||||
contract,
|
||||
)
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.cursor || undefined,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue