feat: note tip

This commit is contained in:
DIYgod 2023-03-15 19:16:56 +00:00
parent f9ff893763
commit 2145811350
No known key found for this signature in database
9 changed files with 189 additions and 50 deletions

View File

@ -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}}"
}

View File

@ -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>

View File

@ -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}

View File

@ -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} />
</>
)

View File

@ -15,7 +15,7 @@ export type Variant =
| "collect"
| "crossbell"
| "outline"
| "patron"
| "tip"
export type VariantColor = "green" | "red" | "gray" | "gradient" | "black"

View File

@ -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}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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,
})
}