feat: 10x token (#354)
* feat: 10x token * feat: power withdraw modal * feat: update token text
This commit is contained in:
parent
77419ac1b9
commit
9088aea651
34
src/hono.ts
34
src/hono.ts
|
|
@ -4362,7 +4362,7 @@ declare const _routes: hono_hono_base.HonoBase<
|
|||
input: {
|
||||
json: {
|
||||
entryId: string
|
||||
amount: "1000000000000000000" | "2000000000000000000"
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
output: {
|
||||
|
|
@ -4465,7 +4465,7 @@ declare const _routes: hono_hono_base.HonoBase<
|
|||
status: 200
|
||||
}
|
||||
}
|
||||
"/wallets/transactions/withdrawable": {
|
||||
"/wallets/transactions/feed-withdrawable": {
|
||||
$get: {
|
||||
input: {
|
||||
query: {
|
||||
|
|
@ -4484,7 +4484,7 @@ declare const _routes: hono_hono_base.HonoBase<
|
|||
status: 200
|
||||
}
|
||||
}
|
||||
"/wallets/transactions/withdraw": {
|
||||
"/wallets/transactions/feed-withdraw": {
|
||||
$post: {
|
||||
input: {
|
||||
json: {
|
||||
|
|
@ -4501,6 +4501,24 @@ declare const _routes: hono_hono_base.HonoBase<
|
|||
status: 200
|
||||
}
|
||||
}
|
||||
"/wallets/transactions/withdraw": {
|
||||
$post: {
|
||||
input: {
|
||||
json: {
|
||||
address: string
|
||||
amount: string
|
||||
}
|
||||
}
|
||||
output: {
|
||||
code: 0
|
||||
data: {
|
||||
transactionHash: string
|
||||
}
|
||||
}
|
||||
outputFormat: "json" | "text"
|
||||
status: 200
|
||||
}
|
||||
}
|
||||
"/wallets/transactions/claim-check": {
|
||||
$get: {
|
||||
input: {}
|
||||
|
|
@ -4544,6 +4562,16 @@ declare const _routes: hono_hono_base.HonoBase<
|
|||
status: 200
|
||||
}
|
||||
}
|
||||
"/wallets/refresh": {
|
||||
$post: {
|
||||
input: {}
|
||||
output: {
|
||||
code: 0
|
||||
}
|
||||
outputFormat: "json" | "text"
|
||||
status: 200
|
||||
}
|
||||
}
|
||||
} & {
|
||||
"/subscriptions": {
|
||||
$get: {
|
||||
|
|
|
|||
|
|
@ -9,3 +9,6 @@ export const QUERY_PERSIST_KEY = getStorageNS("REACT_QUERY_OFFLINE_CACHE")
|
|||
export const ROUTE_FEED_PENDING = "all"
|
||||
export const ROUTE_ENTRY_PENDING = "pending"
|
||||
export const ROUTE_FEED_IN_FOLDER = "folder-"
|
||||
|
||||
export const DAILY_CLAIM_AMOUNT = "20"
|
||||
export const INVITATION_PRICE = "100"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
import * as HoverCard from "@radix-ui/react-hover-card"
|
||||
import { useUISettingKey } from "@renderer/atoms/settings/ui"
|
||||
import { ScrollArea } from "@renderer/components/ui/scroll-area"
|
||||
|
|
|
|||
|
|
@ -267,7 +267,7 @@ const SearchResultCount: FC<{
|
|||
const searchInstance = React.useContext(SearchCmdKContext)
|
||||
const hasKeyword = useSearchStore((s) => !!s.keyword)
|
||||
const searchType = useSearchStore((s) => s.searchType)
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
|
||||
const recordCountPromise = useMemo(async () => {
|
||||
let count = 0
|
||||
const counts = await searchInstance?.then((s) => s.counts)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
TooltipPortal,
|
||||
TooltipTrigger,
|
||||
} from "@renderer/components/ui/tooltip"
|
||||
import { INVITATION_PRICE } from "@renderer/constants"
|
||||
import { useAuthQuery } from "@renderer/hooks/common"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { toastFetchError } from "@renderer/lib/error-parser"
|
||||
|
|
@ -42,8 +43,8 @@ export const SettingInvitations = () => {
|
|||
use.
|
||||
</p>
|
||||
<p className="flex items-center">
|
||||
<span>You can spend 10 </span>
|
||||
<i className="i-mgc-power ml-1 mr-0.5 text-base text-accent" />
|
||||
<span>You can spend {INVITATION_PRICE} </span>
|
||||
<i className="i-mgc-power mx-0.5 text-base text-accent" />
|
||||
<span> Power to generate an invitation code for your friends.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -153,7 +154,7 @@ const ConfirmModalContent = ({ dismiss }: { dismiss: () => void }) => {
|
|||
return (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<span>Generating an invitation code will cost you 10 </span>
|
||||
<span>Generating an invitation code will cost you {INVITATION_PRICE} </span>
|
||||
<i className="i-mgc-power mx-1 text-base text-accent" />
|
||||
<span>Power. Do you want to continue?</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { TooltipTrigger } from "@radix-ui/react-tooltip"
|
||||
import { Button } from "@renderer/components/ui/button"
|
||||
import { Tooltip, TooltipContent } from "@renderer/components/ui/tooltip"
|
||||
import { DAILY_CLAIM_AMOUNT } from "@renderer/constants"
|
||||
import { useClaimCheck, useClaimWalletDailyRewardMutation } from "@renderer/queries/wallet"
|
||||
|
||||
export const ClaimDailyReward = () => {
|
||||
|
|
@ -22,7 +23,9 @@ export const ClaimDailyReward = () => {
|
|||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{canClaim ? "Claim your 2 Daily Power now!" : `You have already claimed today.`}
|
||||
{canClaim
|
||||
? `Claim your ${DAILY_CLAIM_AMOUNT} Daily Power now!`
|
||||
: `You have already claimed today.`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { Button } from "@renderer/components/ui/button"
|
||||
import { CopyButton } from "@renderer/components/ui/code-highlighter"
|
||||
import { LoadingWithIcon } from "@renderer/components/ui/loading"
|
||||
import {
|
||||
|
|
@ -7,9 +8,13 @@ import {
|
|||
TooltipPortal,
|
||||
TooltipTrigger,
|
||||
} from "@renderer/components/ui/tooltip"
|
||||
import { DAILY_CLAIM_AMOUNT } from "@renderer/constants"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { SettingSectionTitle } from "@renderer/modules/settings/section"
|
||||
import { Balance } from "@renderer/modules/wallet/balance"
|
||||
import { useWallet } from "@renderer/queries/wallet"
|
||||
import { useWallet, wallet as walletActions } from "@renderer/queries/wallet"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
|
||||
import { ClaimDailyReward } from "./claim-daily-reward"
|
||||
import { CreateWallet } from "./create-wallet"
|
||||
|
|
@ -20,6 +25,15 @@ export const MyWalletSection = () => {
|
|||
const wallet = useWallet({ userId: user?.id })
|
||||
const myWallet = wallet.data?.[0]
|
||||
|
||||
const refreshMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await apiClient.wallets.refresh.$post()
|
||||
},
|
||||
onSuccess: () => {
|
||||
walletActions.get().invalidate()
|
||||
},
|
||||
})
|
||||
|
||||
if (wallet.isPending) {
|
||||
return (
|
||||
<div className="center absolute inset-0 flex">
|
||||
|
|
@ -51,7 +65,10 @@ export const MyWalletSection = () => {
|
|||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>You can claim 2 free Power daily, which can be used to tip RSS entries on Follow.</p>
|
||||
<p>
|
||||
You can claim {DAILY_CLAIM_AMOUNT} free Power daily, which can be used to tip RSS
|
||||
entries on Follow.
|
||||
</p>
|
||||
</div>
|
||||
<SettingSectionTitle margin="compact" title="Your Address" />
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
|
|
@ -66,9 +83,23 @@ export const MyWalletSection = () => {
|
|||
</div>
|
||||
<SettingSectionTitle title="Your Balance" margin="compact" />
|
||||
<div className="mb-2 flex items-end justify-between">
|
||||
<Balance className="text-xl font-bold text-accent">
|
||||
{BigInt(myWallet.dailyPowerToken || 0n) + BigInt(myWallet.cashablePowerToken || 0n)}
|
||||
</Balance>
|
||||
<div className="flex items-center gap-1">
|
||||
<Balance className="text-xl font-bold text-accent">
|
||||
{BigInt(myWallet.dailyPowerToken || 0n) + BigInt(myWallet.cashablePowerToken || 0n)}
|
||||
</Balance>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => refreshMutation.mutate()}
|
||||
disabled={refreshMutation.isPending}
|
||||
>
|
||||
<i
|
||||
className={cn(
|
||||
"i-mgc-refresh-2-cute-re",
|
||||
refreshMutation.isPending && "animate-spin",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<WithdrawButton />
|
||||
<ClaimDailyReward />
|
||||
|
|
@ -77,25 +108,8 @@ export const MyWalletSection = () => {
|
|||
<Tooltip>
|
||||
<TooltipTrigger className="block">
|
||||
<div className="flex flex-row items-center gap-x-2 text-xs text-zinc-600 dark:text-neutral-400">
|
||||
<span className="flex w-[120px] items-center gap-1 text-left">
|
||||
Daily Power
|
||||
<i className="i-mingcute-question-line" />
|
||||
</span>
|
||||
<Balance>{myWallet.dailyPowerToken}</Balance>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent align="start" className="z-[999]">
|
||||
<p>1. Daily Power can only be used for tipping others.</p>
|
||||
<p>2. You can claim 2 free Daily Power daily.</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="block">
|
||||
<div className="flex flex-row items-center gap-x-2 text-xs text-zinc-600 dark:text-neutral-400">
|
||||
<span className="flex w-[120px] items-center gap-1 text-left">
|
||||
Cashable Power <i className="i-mingcute-question-line" />
|
||||
<span className="flex items-center gap-1 text-left">
|
||||
Withdrawable <i className="i-mingcute-question-line" />
|
||||
</span>
|
||||
|
||||
<Balance>{myWallet.cashablePowerToken}</Balance>
|
||||
|
|
@ -103,9 +117,9 @@ export const MyWalletSection = () => {
|
|||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent align="start" className="z-[999]">
|
||||
<p>1. Cashable Power can be withdrawn to your wallet for trading.</p>
|
||||
<p>
|
||||
2. Cashable Power is the Power you have recharged and the tips you have received.
|
||||
Withdrawable Power includes both the tips you've received and the Power you've
|
||||
recharged.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,137 @@
|
|||
import { TooltipTrigger } from "@radix-ui/react-tooltip"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { Button } from "@renderer/components/ui/button"
|
||||
import { Tooltip, TooltipContent } from "@renderer/components/ui/tooltip"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@renderer/components/ui/form"
|
||||
import { Input } from "@renderer/components/ui/input"
|
||||
import { useModalStack } from "@renderer/components/ui/modal"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { cn } from "@renderer/lib/utils"
|
||||
import { Balance } from "@renderer/modules/wallet/balance"
|
||||
import { useWallet, wallet as walletActions } from "@renderer/queries/wallet"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { from, toNumber } from "dnum"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
export const WithdrawButton = () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant="outline" disabled={true}>
|
||||
Withdraw
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Comming soon...</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
export const WithdrawButton = () => {
|
||||
const { present } = useModalStack()
|
||||
|
||||
const onClick = () => {
|
||||
present({
|
||||
title: "Withdraw Power",
|
||||
content: ({ dismiss }) => <WithdrawModalContent dismiss={dismiss} />,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="outline" onClick={onClick}>
|
||||
Withdraw
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const WithdrawModalContent = ({ dismiss }: { dismiss: () => void }) => {
|
||||
const user = useWhoami()
|
||||
const wallet = useWallet({ userId: user?.id })
|
||||
const cashablePowerTokenBigInt = [BigInt(wallet.data?.[0].cashablePowerToken || 0n), 18] as const
|
||||
const cashablePowerTokenNumber = toNumber(cashablePowerTokenBigInt)
|
||||
|
||||
const formSchema = z.object({
|
||||
address: z.string().startsWith("0x").length(42),
|
||||
amount: z.number().min(0).max(cashablePowerTokenNumber),
|
||||
})
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
})
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async ({ address, amount }: { address: string; amount: number }) => {
|
||||
const amountBigInt = from(amount, 18)[0]
|
||||
await apiClient.wallets.transactions.withdraw.$post({
|
||||
json: {
|
||||
address,
|
||||
amount: amountBigInt.toString(),
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
mutation.mutate(values)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (mutation.isError) {
|
||||
toast.error(`Withdrawal failed: ${mutation.error?.message}`)
|
||||
}
|
||||
}, [mutation.isError])
|
||||
|
||||
useEffect(() => {
|
||||
if (mutation.isSuccess) {
|
||||
toast.success("Withdrawal successful!")
|
||||
walletActions.get().invalidate()
|
||||
walletActions.transactions.get().invalidate()
|
||||
dismiss()
|
||||
}
|
||||
}, [mutation.isSuccess])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(!cashablePowerTokenNumber && "text-orange-700", "mb-4 text-sm")}>
|
||||
You have{" "}
|
||||
<Balance className="inline-block">{wallet.data?.[0].cashablePowerToken || "0"}</Balance>{" "}
|
||||
withdrawable Power in your wallet.
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-96 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Your Ethereum Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input autoFocus {...field} placeholder="0x..." />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Amount</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
autoFocus
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(value) => field.onChange(value.target.valueAsNumber)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="center flex">
|
||||
<Button disabled={!form.formState.isValid} type="submit" isLoading={mutation.isPending}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ export const Balance = ({
|
|||
const formattedFull = format(n, { digits: 18, trailingZeros: true })
|
||||
|
||||
const Content = (
|
||||
<div className={cn("font-mono tabular-nums", className)}>
|
||||
{formatted} {withSuffix && <span>POWER</span>}
|
||||
<div className={cn("tabular-nums", className)}>
|
||||
{formatted} {withSuffix && <span>Power</span>}
|
||||
</div>
|
||||
)
|
||||
|
||||
|
|
@ -33,8 +33,8 @@ export const Balance = ({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>{Content}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-mono text-sm">
|
||||
<span className="font-bold tabular-nums">{formattedFull}</span> <span>POWER</span>
|
||||
<div className="text-sm">
|
||||
<span className="font-bold tabular-nums">{formattedFull}</span> <span>Power</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useFeedClaimModal } from "../claim"
|
|||
import { useSettingModal } from "../settings/modal/hooks-hack"
|
||||
import { Balance } from "./balance"
|
||||
|
||||
const DEFAULT_RECOMMENDED_TIP = 1
|
||||
const DEFAULT_RECOMMENDED_TIP = 10
|
||||
|
||||
const useMyWallet = () => {
|
||||
const user = useWhoami()
|
||||
|
|
@ -56,7 +56,7 @@ const TipModalContent_: FC<{
|
|||
|
||||
const tipMutation = useWalletTipMutation()
|
||||
|
||||
const [amount, setAmount] = useState<1 | 2>(DEFAULT_RECOMMENDED_TIP)
|
||||
const [amount, setAmount] = useState<number>(DEFAULT_RECOMMENDED_TIP)
|
||||
|
||||
const amountBigInt = from(amount, 18)[0]
|
||||
|
||||
|
|
@ -146,13 +146,10 @@ const TipModalContent_: FC<{
|
|||
<span>Amount</span>
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
value={amount.toString()}
|
||||
onValueChange={(value) => setAmount(Number(value) as 1 | 2)}
|
||||
>
|
||||
<RadioGroup value={amount.toString()} onValueChange={(value) => setAmount(Number(value))}>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<RadioCard wrapperClassName="justify-center" label="1 Power" value="1" />
|
||||
<RadioCard wrapperClassName="justify-center" label="2 Power" value="2" />
|
||||
<RadioCard wrapperClassName="justify-center" label="10 Power" value="10" />
|
||||
<RadioCard wrapperClassName="justify-center" label="20 Power" value="20" />
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const I18nProvider: FC<PropsWithChildren> = ({ children }) => {
|
|||
const [currentI18NInstance, update] = useAtom(i18nAtom)
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/rules-of-hooks
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(
|
||||
() =>
|
||||
EventBus.subscribe("I18N_UPDATE", () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import type { EntryModel } from "@renderer/models"
|
||||
import {
|
||||
EntryService,
|
||||
FeedService,
|
||||
SubscriptionService,
|
||||
} from "@renderer/services"
|
||||
import { EntryService, FeedService, SubscriptionService } from "@renderer/services"
|
||||
import type { IFuseOptions } from "fuse.js"
|
||||
import Fuse from "fuse.js"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue