parent
2ff6fc0082
commit
5cc7898e1e
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6336 24.5001C19.2755 24.5001 24.6598 19.1158 24.6598 12.4739C24.6598 5.83206 19.2755 0.447754 12.6336 0.447754C5.99173 0.447754 0.607422 5.83206 0.607422 12.4739C0.607422 19.1158 5.99173 24.5001 12.6336 24.5001ZM10.3646 13.5043H7.87669C7.0796 13.5043 6.65288 12.5623 7.17622 11.9584L13.2711 5.03422C13.8347 4.39011 14.8975 4.79268 14.8975 5.64613V11.4431H17.3854C18.1906 11.4431 18.6173 12.3852 18.0859 12.989L11.991 19.9132C11.4274 20.5573 10.3646 20.1548 10.3646 19.3013V13.5043Z" fill="#FF5C00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 656 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6336 24.5001C19.2755 24.5001 24.6598 19.1158 24.6598 12.4739C24.6598 5.83206 19.2755 0.447754 12.6336 0.447754C5.99173 0.447754 0.607422 5.83206 0.607422 12.4739C0.607422 19.1158 5.99173 24.5001 12.6336 24.5001ZM10.3646 13.5043H7.87669C7.0796 13.5043 6.65288 12.5623 7.17622 11.9584L13.2711 5.03422C13.8347 4.39011 14.8975 4.79268 14.8975 5.64613V11.4431H17.3854C18.1906 11.4431 18.6173 12.3852 18.0859 12.989L11.991 19.9132C11.4274 20.5573 10.3646 20.1548 10.3646 19.3013V13.5043Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 661 B |
|
|
@ -59,6 +59,7 @@
|
|||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"dayjs": "1.11.11",
|
||||
"dnum": "^2.13.1",
|
||||
"dotenv": "16.4.5",
|
||||
"electron-updater": "^6.2.1",
|
||||
"font-list": "1.5.1",
|
||||
|
|
|
|||
|
|
@ -118,6 +118,9 @@ importers:
|
|||
dayjs:
|
||||
specifier: 1.11.11
|
||||
version: 1.11.11
|
||||
dnum:
|
||||
specifier: ^2.13.1
|
||||
version: 2.13.1
|
||||
dotenv:
|
||||
specifier: 16.4.5
|
||||
version: 16.4.5
|
||||
|
|
@ -3090,6 +3093,9 @@ packages:
|
|||
os: [darwin]
|
||||
hasBin: true
|
||||
|
||||
dnum@2.13.1:
|
||||
resolution: {integrity: sha512-4oZ+BtlvNtKFJji1Fc5073LyJFvgioBQ0PNu/C+r1A8P09Yvka/aXYYD5bsUHMTUPEu01iv4bk+5nPQmt5AA8A==}
|
||||
|
||||
doctrine@3.0.0:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
@ -3569,6 +3575,9 @@ packages:
|
|||
franc-min@6.2.0:
|
||||
resolution: {integrity: sha512-1uDIEUSlUZgvJa2AKYR/dmJC66v/PvGQ9mWfI9nOr/kPpMFyvswK0gPXOwpYJYiYD008PpHLkGfG58SPjQJFxw==}
|
||||
|
||||
from-exponential@1.1.1:
|
||||
resolution: {integrity: sha512-VBE7f5OVnYwdgB3LHa+Qo29h8qVpxhVO9Trlc+AWm+/XNAgks1tAwMFHb33mjeiof77GglsJzeYF7OqXrROP/A==}
|
||||
|
||||
fs-constants@1.0.0:
|
||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||
|
||||
|
|
@ -8958,6 +8967,10 @@ snapshots:
|
|||
verror: 1.10.1
|
||||
optional: true
|
||||
|
||||
dnum@2.13.1:
|
||||
dependencies:
|
||||
from-exponential: 1.1.1
|
||||
|
||||
doctrine@3.0.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
|
@ -9662,6 +9675,8 @@ snapshots:
|
|||
dependencies:
|
||||
trigram-utils: 2.0.1
|
||||
|
||||
from-exponential@1.1.1: {}
|
||||
|
||||
fs-constants@1.0.0: {}
|
||||
|
||||
fs-extra@10.1.0:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
import { cn } from "@renderer/lib/utils"
|
||||
import { format } from "dnum"
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip"
|
||||
|
||||
export const Balance = ({
|
||||
children,
|
||||
className,
|
||||
precision = 2,
|
||||
withSuffix = false,
|
||||
}: {
|
||||
/** The token balance in wei. */
|
||||
children: bigint
|
||||
className?: string
|
||||
precision?: number
|
||||
withSuffix?: boolean
|
||||
}) => {
|
||||
const from = [BigInt(children), 18] as const
|
||||
const formatted = format(from, { digits: precision, trailingZeros: true })
|
||||
const formattedFull = format(from, { digits: 18, trailingZeros: true })
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className={cn("font-mono tabular-nums", className)}>
|
||||
{formatted}
|
||||
{" "}
|
||||
{withSuffix && <span>$POWER</span>}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-mono text-sm">
|
||||
<span className="font-bold tabular-nums">{formattedFull}</span>
|
||||
{" "}
|
||||
<span>$POWER</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -136,6 +136,14 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
|
|||
// Here we need to delay one frame, so it's two raf,
|
||||
// in order to have `point-event: none` recorded by RadixOverlay after modal is invoked in a certain scenario,
|
||||
// and the page freezes after modal is turned off.
|
||||
nextFrame(() => settingModalPresent("wallet"))
|
||||
}}
|
||||
>
|
||||
Wallet
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
nextFrame(settingModalPresent)
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,74 @@ declare const routes: hono_hono_base.HonoBase<hono_types.BlankEnv, {
|
|||
status: 200;
|
||||
};
|
||||
};
|
||||
"/wallets/transactions": {
|
||||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
type?: "tip" | "mint" | "burn" | "withdraw" | undefined;
|
||||
hash?: string | undefined;
|
||||
fromUserId?: string | undefined;
|
||||
toUserId?: string | undefined;
|
||||
toEntryId?: string | undefined;
|
||||
fromOrToUserId?: string | undefined;
|
||||
createdAfter?: string | undefined;
|
||||
};
|
||||
};
|
||||
output: {
|
||||
code: 0;
|
||||
data: {
|
||||
type: "tip" | "mint" | "burn" | "withdraw";
|
||||
createdAt: string;
|
||||
powerToken: bigint;
|
||||
hash: string;
|
||||
fromUserId: string | null;
|
||||
toUserId: string | null;
|
||||
toEntryId: string | null;
|
||||
comment: string | null;
|
||||
fromUser?: {
|
||||
name: string | null;
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: Date | null;
|
||||
image: string | null;
|
||||
handle: string | null;
|
||||
} | undefined;
|
||||
toUser?: {
|
||||
name: string | null;
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: Date | null;
|
||||
image: string | null;
|
||||
handle: string | null;
|
||||
} | undefined;
|
||||
toEntry?: {
|
||||
description: string | null;
|
||||
title: string | null;
|
||||
content: string | null;
|
||||
id: string;
|
||||
author: string | null;
|
||||
url: string | null;
|
||||
feedId: string;
|
||||
guid: string;
|
||||
authorUrl: string | null;
|
||||
authorAvatar: string | null;
|
||||
changedAt: string;
|
||||
publishedAt: string;
|
||||
images: string[] | null;
|
||||
categories: string[] | null;
|
||||
enclosures?: {
|
||||
url: string;
|
||||
length?: number | undefined;
|
||||
type?: string | undefined;
|
||||
title?: string | undefined;
|
||||
}[] | null | undefined;
|
||||
} | undefined;
|
||||
}[];
|
||||
};
|
||||
outputFormat: "json";
|
||||
status: 200;
|
||||
};
|
||||
};
|
||||
"/wallets/transactions/claim_daily": {
|
||||
$post: {
|
||||
input: {};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { nextFrame } from "@renderer/lib/dom"
|
||||
import { shortcuts } from "@renderer/lib/shortcuts"
|
||||
import type { EntryModel } from "@renderer/models"
|
||||
import { useTipModal } from "@renderer/modules/wallet/hooks"
|
||||
import { entryActions } from "@renderer/store"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import type { FetchError } from "ofetch"
|
||||
|
|
@ -112,14 +114,32 @@ export const useEntryActions = ({
|
|||
refetchOnWindowFocus: false,
|
||||
})
|
||||
|
||||
// const canTip = Boolean(entry?.feeds.ownerUserId)
|
||||
// comment out L119-122 to work ⬇️
|
||||
const openTipModal = useTipModal({
|
||||
userId: entry?.feeds.ownerUserId ?? undefined,
|
||||
entryId: entry?.entries.id ?? undefined,
|
||||
})
|
||||
|
||||
const collect = useCollect(entry)
|
||||
const uncollect = useUnCollect(entry)
|
||||
const read = useRead()
|
||||
const unread = useUnread()
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!entry || view === undefined) return []
|
||||
const items = [
|
||||
[
|
||||
{
|
||||
key: "tip",
|
||||
shortcut: shortcuts.entry.tip.key,
|
||||
name: `Tip`,
|
||||
className: "i-mgc-power-mono",
|
||||
// disabled: !canTip,
|
||||
onClick: () => {
|
||||
nextFrame(openTipModal)
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "star",
|
||||
shortcut: shortcuts.entry.toggleStarred.key,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,10 @@ export const shortcuts = {
|
|||
copyLink: {
|
||||
name: "Copy Link",
|
||||
key: "Meta+Shift+C",
|
||||
|
||||
},
|
||||
tip: {
|
||||
name: "Tip",
|
||||
key: "Meta+Shift+T",
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable tailwindcss/no-custom-classname */
|
||||
import { Button } from "@renderer/components/ui/button"
|
||||
import { Card, CardHeader } from "@renderer/components/ui/card"
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
import { SettingsTitle } from "../../title"
|
||||
import { MyWalletSection } from "./my-wallet-section"
|
||||
import { TransactionsSection } from "./transaction-section"
|
||||
|
||||
export const SettingWallet = () => (
|
||||
<div>
|
||||
<SettingsTitle />
|
||||
|
||||
<MyWalletSection />
|
||||
|
||||
<TransactionsSection />
|
||||
</div>
|
||||
)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { TooltipTrigger } from "@radix-ui/react-tooltip"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { Tooltip, TooltipContent } from "@renderer/components/ui/tooltip"
|
||||
import {
|
||||
useClaimWalletDailyRewardMutation,
|
||||
useClaimWalletDailyRewardTtl,
|
||||
} from "@renderer/queries/wallet"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export const ClaimDailyReward = () => {
|
||||
const mutation = useClaimWalletDailyRewardMutation()
|
||||
|
||||
const ttl = useClaimWalletDailyRewardTtl()
|
||||
|
||||
const [ttlState, setTtlState] = useState(ttl.data?.data.ttl ?? 0)
|
||||
|
||||
const hour = Math.floor(ttlState / 3600)
|
||||
.toString()
|
||||
.padStart(2, "0")
|
||||
const minute = Math.floor((ttlState % 3600) / 60)
|
||||
.toString()
|
||||
.padStart(2, "0")
|
||||
const second = (ttlState % 60).toString().padStart(2, "0")
|
||||
|
||||
const canClaim = ttlState <= 0
|
||||
|
||||
// refresh ttl every second
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTtlState((prev) => (prev > 0 ? prev - 1 : 0))
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
// update ttl when ttl changes
|
||||
useEffect(() => {
|
||||
if (ttl.data?.data.ttl) {
|
||||
setTtlState(ttl.data.data.ttl)
|
||||
}
|
||||
}, [ttl.data?.data.ttl])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<StyledButton
|
||||
variant="primary"
|
||||
isLoading={mutation.isPending}
|
||||
onClick={() => mutation.mutate()}
|
||||
disabled={!canClaim}
|
||||
>
|
||||
{canClaim ?
|
||||
"Claim Daily Reward" :
|
||||
`Reward in ${hour}:${minute}:${second}`}
|
||||
</StyledButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canClaim ?
|
||||
"Claim your daily reward (2.00 PowerTokens) now!" :
|
||||
`You can claim your daily reward in ${hour}:${minute}:${second}.`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { useCreateWalletMutation } from "@renderer/queries/wallet"
|
||||
|
||||
export const CreateWallet = () => {
|
||||
const mutation = useCreateWalletMutation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledButton
|
||||
variant="primary"
|
||||
isLoading={mutation.isPending}
|
||||
onClick={() => mutation.mutate()}
|
||||
>
|
||||
Create Wallet
|
||||
</StyledButton>
|
||||
|
||||
<p className="my-2 text-theme-inactive">
|
||||
Create a free wallet to receive
|
||||
{" "}
|
||||
<strong>PowerTokens</strong>
|
||||
, which can
|
||||
be used to reward creators and also get rewarded for your content
|
||||
contributions.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { useUser } from "@renderer/atoms/user"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
import { Balance } from "@renderer/components/ui/wallet/balance"
|
||||
import { useWallet } from "@renderer/queries/wallet"
|
||||
|
||||
import { SettingSectionTitle } from "../../../section"
|
||||
import { ClaimDailyReward } from "./claim-daily-reward"
|
||||
import { CreateWallet } from "./create-wallet"
|
||||
|
||||
export const MyWalletSection = () => {
|
||||
const user = useUser()
|
||||
const wallet = useWallet({ userId: user?.id })
|
||||
const myWallet = wallet.data?.[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingSectionTitle title="My Wallet" />
|
||||
|
||||
{wallet.isPending ? (
|
||||
<LoadingCircle size="large" />
|
||||
) : !myWallet ?
|
||||
(
|
||||
<CreateWallet />
|
||||
) :
|
||||
(
|
||||
<div className="flex flex-row items-center gap-x-5">
|
||||
<div className="font-bold">Balance</div>
|
||||
<Balance withSuffix>{myWallet.powerToken}</Balance>
|
||||
<ClaimDailyReward />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { useUser } from "@renderer/atoms/user"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@renderer/components/ui/avatar"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@renderer/components/ui/table"
|
||||
import { Balance } from "@renderer/components/ui/wallet/balance"
|
||||
import dayjs from "@renderer/lib/dayjs"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { SettingSectionTitle } from "@renderer/modules/settings/section"
|
||||
import { useWallet, useWalletTransactions } from "@renderer/queries/wallet"
|
||||
|
||||
export const TransactionsSection = () => {
|
||||
const user = useUser()
|
||||
const wallet = useWallet({ userId: user?.id })
|
||||
const myWallet = wallet.data?.[0]
|
||||
|
||||
const transactions = useWalletTransactions({ fromOrToUserId: user?.id })
|
||||
|
||||
if (!myWallet) return
|
||||
|
||||
if (transactions.isLoading) return <div className="text-theme-disabled">Loading...</div>
|
||||
|
||||
if (transactions.data?.length === 0) return <div className="text-theme-disabled">No transactions</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingSectionTitle title="Transactions" />
|
||||
|
||||
<div className="overflow-x-scroll">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead size="sm" className="uppercase">Type</TableHead>
|
||||
<TableHead size="sm" className="uppercase">Amount</TableHead>
|
||||
<TableHead size="sm" className="uppercase">From</TableHead>
|
||||
<TableHead size="sm" className="uppercase">To</TableHead>
|
||||
<TableHead size="sm" className="uppercase">Entry</TableHead>
|
||||
<TableHead size="sm" className="uppercase">Date</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{transactions.data?.map(
|
||||
(row) => (
|
||||
<TableRow key={row.hash}>
|
||||
<TableCell align="center" size="sm"><TypeRenderer>{row.type}</TypeRenderer></TableCell>
|
||||
<TableCell align="center" size="sm"><Balance>{row.powerToken}</Balance></TableCell>
|
||||
<TableCell align="center" size="sm"><UserRenderer user={row.fromUser} /></TableCell>
|
||||
<TableCell align="center" size="sm"><UserRenderer user={row.toUser} /></TableCell>
|
||||
<TableCell align="center" size="sm"><EntryRenderer entry={row.toEntry} /></TableCell>
|
||||
<TableCell align="center" size="sm">{dayjs(row.createdAt).fromNow()}</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TypeRenderer = ({ children }: { children: NonNullable<ReturnType<typeof useWalletTransactions>["data"]>[number]["type"] }) => (
|
||||
<div className={cn("center rounded-full p-px text-xs uppercase", {
|
||||
"bg-theme-primary text-white": children === "tip",
|
||||
"bg-green-700 text-white": children === "mint",
|
||||
"bg-red-700 text-white": children === "burn",
|
||||
"bg-yellow-700 text-white": children === "withdraw",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const UserRenderer = ({ user }: { user?: NonNullable<ReturnType<typeof useWalletTransactions>["data"]>[number]["fromUser" | "toUser"] }) => {
|
||||
const me = useUser()
|
||||
const isMe = user?.id === me?.id
|
||||
|
||||
return (
|
||||
<div className="center">
|
||||
<Avatar className="aspect-square size-4">
|
||||
<AvatarImage src={user?.image || undefined} />
|
||||
<AvatarFallback>{user?.name?.slice(0, 2)}</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="ml-1">
|
||||
{isMe ? <span className="font-bold">You</span> : user?.name || "NULL"}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const EntryRenderer = ({ entry }: { entry: NonNullable<ReturnType<typeof useWalletTransactions>["data"]>[number]["toEntry"] }) => (
|
||||
<div className="center line-clamp-1 truncate">
|
||||
{entry?.title ?? "-"}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { useModalStack } from "@renderer/components/ui/modal/stacked/hooks"
|
||||
import { createElement, useCallback } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { TipModalContent } from "./tip-modal"
|
||||
|
||||
export const useTipModal = ({ userId, entryId }: { userId?: string, entryId?: string }) => {
|
||||
const { present } = useModalStack()
|
||||
|
||||
return useCallback(() => {
|
||||
if (!userId) {
|
||||
// this should not happen unless there is a bug in the code
|
||||
toast.error("You must have a user to tip.")
|
||||
return
|
||||
}
|
||||
|
||||
present({
|
||||
title: "Tip",
|
||||
content: () => createElement(TipModalContent, { userId, entryId }),
|
||||
})
|
||||
}, [present, userId, entryId])
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
import { useWalletTipMutation, useWalletTransactions } from "@renderer/queries/wallet"
|
||||
import type { FC } from "react"
|
||||
|
||||
export const TipModalContent: FC<{
|
||||
userId: string
|
||||
entryId?: string
|
||||
}> = ({ userId, entryId }) => {
|
||||
const transationsQuery = useWalletTransactions({ toUserId: userId, toEntryId: entryId })
|
||||
|
||||
const tipMutation = useWalletTipMutation()
|
||||
|
||||
if (transationsQuery.isPending) {
|
||||
return (
|
||||
<div className="center h-32 w-[650px]">
|
||||
<LoadingCircle size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (transationsQuery.error) {
|
||||
return <div>Failed to load transactions history.</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-[650px] max-w-full">
|
||||
{/* // TODO: */}
|
||||
<div className="mt-3 flex justify-end">
|
||||
<StyledButton
|
||||
disabled={tipMutation.isSuccess || tipMutation.isPending}
|
||||
isLoading={tipMutation.isPending}
|
||||
onClick={() => tipMutation.mutate({ userId, entryId, amount: "0" })}
|
||||
variant={tipMutation.isSuccess ? "plain" : "primary"}
|
||||
>
|
||||
{tipMutation.isSuccess && (
|
||||
<i className="i-mgc-check-circle-filled mr-2 bg-green-500" />
|
||||
)}
|
||||
Claim
|
||||
</StyledButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { SettingWallet } from "@renderer/modules/settings/tabs/wallet"
|
||||
import { defineSettingPage } from "@renderer/modules/settings/utils"
|
||||
|
||||
const iconName = "i-mgc-power"
|
||||
const name = "Wallet"
|
||||
const priority = 1050
|
||||
|
||||
export const loader = defineSettingPage({
|
||||
iconName,
|
||||
name,
|
||||
priority,
|
||||
})
|
||||
|
||||
export function Component() {
|
||||
return <SettingWallet />
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import { discover } from "./discover"
|
|||
import { entries } from "./entries"
|
||||
import { feed } from "./feed"
|
||||
import { subscription } from "./subscriptions"
|
||||
import { wallet } from "./wallet"
|
||||
|
||||
export const Queries = {
|
||||
subscription,
|
||||
|
|
@ -14,4 +15,5 @@ export const Queries = {
|
|||
auth,
|
||||
ai,
|
||||
discover,
|
||||
wallet,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import { useBizQuery } from "@renderer/hooks"
|
||||
import { apiClient, getFetchErrorMessage } from "@renderer/lib/api-fetch"
|
||||
import { defineQuery } from "@renderer/lib/defineQuery"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export const wallet = {
|
||||
get: ({ userId }: { userId?: string } = {}) =>
|
||||
defineQuery(
|
||||
["wallet", userId].filter(Boolean),
|
||||
async () => {
|
||||
const res = await apiClient.wallets.$get({
|
||||
query: { userId },
|
||||
})
|
||||
|
||||
return res.data
|
||||
},
|
||||
{
|
||||
rootKey: ["wallet"],
|
||||
},
|
||||
),
|
||||
|
||||
claimDailyRewardTtl: () =>
|
||||
defineQuery(["wallet", "claimDailyRewardTtl"], async () =>
|
||||
apiClient.wallets.transactions.claim_daily_ttl.$get()),
|
||||
|
||||
transactions: {
|
||||
get: (query: Parameters<typeof apiClient.wallets.transactions.$get>[0]["query"] = {}) =>
|
||||
defineQuery(
|
||||
["wallet", "transactions", query].filter(Boolean),
|
||||
async () => {
|
||||
const res = await apiClient.wallets.transactions.$get({ query })
|
||||
|
||||
return res.data
|
||||
},
|
||||
{
|
||||
rootKey: ["wallet", "transactions"],
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const useWallet = ({ userId }: { userId?: string } = {}) =>
|
||||
useBizQuery(
|
||||
wallet.get({ userId }),
|
||||
{ enabled: !!userId },
|
||||
)
|
||||
|
||||
export const useWalletTransactions = (query: Parameters<typeof wallet.transactions.get>[0] = {}) =>
|
||||
useBizQuery(
|
||||
wallet.transactions.get(query),
|
||||
)
|
||||
|
||||
export const useCreateWalletMutation = () =>
|
||||
useMutation({
|
||||
mutationKey: ["createWallet"],
|
||||
mutationFn: () => apiClient.wallets.$post(),
|
||||
async onError(err) {
|
||||
toast.error(await getFetchErrorMessage(err))
|
||||
},
|
||||
onSuccess() {
|
||||
wallet.get().invalidate()
|
||||
toast("🎉 Wallet created.")
|
||||
},
|
||||
})
|
||||
|
||||
export const useClaimWalletDailyRewardTtl = () =>
|
||||
useBizQuery(
|
||||
wallet.claimDailyRewardTtl(),
|
||||
{ refetchInterval: 5000 },
|
||||
)
|
||||
|
||||
export const useClaimWalletDailyRewardMutation = () => useMutation({
|
||||
mutationKey: ["claimWalletDailyReward"],
|
||||
mutationFn: () => apiClient.wallets.transactions.claim_daily.$post(),
|
||||
async onError(err) {
|
||||
toast.error(await getFetchErrorMessage(err))
|
||||
},
|
||||
onSuccess() {
|
||||
wallet.get().invalidate()
|
||||
wallet.claimDailyRewardTtl().invalidate()
|
||||
toast("🎉 Daily reward claimed.")
|
||||
},
|
||||
})
|
||||
|
||||
export const useWalletTipMutation = () => useMutation({
|
||||
mutationKey: ["walletTip"],
|
||||
mutationFn: (data: { userId: string, entryId?: string, amount: string }) =>
|
||||
apiClient.wallets.transactions.tip.$post({ json: data }),
|
||||
async onError(err) {
|
||||
toast.error(await getFetchErrorMessage(err))
|
||||
},
|
||||
onSuccess() {
|
||||
wallet.get().invalidate()
|
||||
wallet.transactions.get().invalidate()
|
||||
toast("🎉 Tipped.")
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue