diff --git a/apps/renderer/src/hooks/biz/useFeedActions.tsx b/apps/renderer/src/hooks/biz/useFeedActions.tsx index d66e65ef0d..ef9e24dbd5 100644 --- a/apps/renderer/src/hooks/biz/useFeedActions.tsx +++ b/apps/renderer/src/hooks/biz/useFeedActions.tsx @@ -9,6 +9,7 @@ import type { FeedViewType } from "~/lib/enum" import type { NativeMenuItem, NullableNativeMenuItem } from "~/lib/native-menu" import { UrlBuilder } from "~/lib/url-builder" import { isBizId } from "~/lib/utils" +import { useBoostModal } from "~/modules/boost/hooks" import { useFeedClaimModal } from "~/modules/claim" import { FeedForm } from "~/modules/discover/feed-form" import { InboxForm } from "~/modules/discover/inbox-form" @@ -52,6 +53,7 @@ export const useFeedActions = ({ const { mutateAsync: addFeedToListMutation } = useAddFeedToFeedList() const { mutateAsync: removeFeedFromListMutation } = useRemoveFeedFromFeedList() + const openBoostModal = useBoostModal() const listByView = useOwnedList(view!) @@ -86,6 +88,13 @@ export const useFeedActions = ({ }, ] : []), + { + type: "text" as const, + label: "Boost", + click: () => { + openBoostModal(feedId) + }, + }, { type: "separator" as const, disabled: isEntryList, diff --git a/apps/renderer/src/modules/boost/hooks.tsx b/apps/renderer/src/modules/boost/hooks.tsx new file mode 100644 index 0000000000..fa4a4ce1b3 --- /dev/null +++ b/apps/renderer/src/modules/boost/hooks.tsx @@ -0,0 +1,21 @@ +import { useCallback } from "react" + +import { useModalStack } from "~/components/ui/modal" + +import { BoostModalContent } from "./modal" + +export const useBoostModal = () => { + const { present } = useModalStack() + + return useCallback( + (feedId: string) => { + present({ + id: "boost", + title: "Boost", + content: () => , + overlay: true, + }) + }, + [present], + ) +} diff --git a/apps/renderer/src/modules/boost/modal.tsx b/apps/renderer/src/modules/boost/modal.tsx new file mode 100644 index 0000000000..b6a104f70b --- /dev/null +++ b/apps/renderer/src/modules/boost/modal.tsx @@ -0,0 +1,137 @@ +import { from } from "dnum" +import { useCallback, useState } from "react" + +import { Button } from "~/components/ui/button" +import { LoadingWithIcon } from "~/components/ui/loading" +import { useCurrentModal } from "~/components/ui/modal" +import { useAuthQuery, useI18n } from "~/hooks/common" +import { boosts, useBoostFeedMutation } from "~/queries/boosts" +import { useWallet } from "~/queries/wallet" + +import { Balance } from "../wallet/balance" +import { RadioCards } from "./radio-cards" + +export const BoostModalContent = ({ feedId }: { feedId: string }) => { + const t = useI18n() + const myWallet = useWallet() + const myWalletData = myWallet.data?.[0] + + const dPowerBigInt = BigInt(myWalletData?.dailyPowerToken ?? 0) + const cPowerBigInt = BigInt(myWalletData?.cashablePowerToken ?? 0) + const balanceBigInt = cPowerBigInt + dPowerBigInt + const [amount, setAmount] = useState(0) + const amountBigInt = from(amount, 18)[0] + const wrongNumberRange = amountBigInt > balanceBigInt || amountBigInt <= BigInt(0) + + const { data: boostStatus, isLoading } = useAuthQuery(boosts.getStatus({ feedId })) + const boostFeedMutation = useBoostFeedMutation() + const { dismiss } = useCurrentModal() + + const handleBoost = useCallback(() => { + if (boostFeedMutation.isPending) return + boostFeedMutation.mutate({ feedId, amount: amountBigInt.toString() }) + }, [amountBigInt, boostFeedMutation, feedId]) + + if (isLoading || !boostStatus) { + return ( +
+ } size="large" /> +
+ ) + } + + if (boostFeedMutation.isSuccess) { + return ( +
+

{t("tip_modal.tip_sent")}

+ +

+ + {amountBigInt} + {" "} + {t("tip_modal.tip_amount_sent")} +

+ +
+ +
+
+ ) + } + + return ( +
+
+
🚀 Boost Feed
+ + + Boost feed to get more privilege, everyone subscribed to this feed will thank you. + + + + +
+ + +
+ ) +} + +const BoostProgress = ({ + level, + boostCount, + remainingBoostsToLevelUp, +}: { + level: number + boostCount: number + remainingBoostsToLevelUp: number +}) => { + const percentage = (boostCount / (boostCount + remainingBoostsToLevelUp)) * 100 + const nextLevel = level + 1 + return ( +
+
+ + ⚡️ {boostCount} + +
+
+
+
+ +
+ Lv. {level} + Lv. {nextLevel} +
+ + {remainingBoostsToLevelUp} more boost will unlock the next level of privilege. + +
+ ) +} diff --git a/apps/renderer/src/modules/boost/radio-cards.tsx b/apps/renderer/src/modules/boost/radio-cards.tsx new file mode 100644 index 0000000000..e7b2d49715 --- /dev/null +++ b/apps/renderer/src/modules/boost/radio-cards.tsx @@ -0,0 +1,54 @@ +import { RadioGroup } from "~/components/ui/radio-group" +import { RadioCard } from "~/components/ui/radio-group/RadioCard" + +const radios = [ + { + name: "1 Month", + value: 1, + }, + { + name: "3 Months", + value: 3, + }, + { + name: "6 Months", + value: 6, + }, + { + name: "1 Year", + value: 12, + }, +] + +export const RadioCards = ({ + monthlyBoostCost, + value, + onValueChange, +}: { + monthlyBoostCost: number + value: number + onValueChange: (value: number) => void +}) => { + return ( + onValueChange(+value)}> +
+ {radios.map((item) => ( + +

{item.name}

+

+ {item.value * monthlyBoostCost} + +

+
+ } + /> + ))} +
+ + ) +} diff --git a/apps/renderer/src/queries/boosts.tsx b/apps/renderer/src/queries/boosts.tsx new file mode 100644 index 0000000000..74c54ed83a --- /dev/null +++ b/apps/renderer/src/queries/boosts.tsx @@ -0,0 +1,36 @@ +import { useMutation } from "@tanstack/react-query" +import { toast } from "sonner" + +import { apiClient } from "~/lib/api-fetch" +import { defineQuery } from "~/lib/defineQuery" +import { toastFetchError } from "~/lib/error-parser" + +export const boosts = { + getStatus: ({ feedId }: { feedId: string }) => + defineQuery(["boostFeed", feedId], async () => { + const res = await apiClient.boosts.$get({ + query: { + feedId, + }, + }) + return res.data + }), +} + +export const useBoostFeedMutation = () => + useMutation({ + mutationKey: ["boostFeed"], + mutationFn: (data: Parameters[0]["json"]) => + apiClient.boosts.$post({ json: data }), + onError(err) { + toastFetchError(err) + }, + onSuccess(_, variables) { + boosts.getStatus({ feedId: variables.feedId }).invalidate() + window.analytics?.capture("boost_sent", { + amount: variables.amount, + feedId: variables.feedId, + }) + toast("🎉 Boosted.") + }, + })