diff --git a/apps/renderer/src/modules/power/my-wallet-section/index.tsx b/apps/renderer/src/modules/power/my-wallet-section/index.tsx index 94901d8532..0b67388c72 100644 --- a/apps/renderer/src/modules/power/my-wallet-section/index.tsx +++ b/apps/renderer/src/modules/power/my-wallet-section/index.tsx @@ -3,7 +3,6 @@ import { Trans, useTranslation } from "react-i18next" import { Button } from "~/components/ui/button" import { CopyButton } from "~/components/ui/code-highlighter" -import { Divider } from "~/components/ui/divider" import { LoadingWithIcon } from "~/components/ui/loading" import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "~/components/ui/tooltip" import { DAILY_CLAIM_AMOUNT } from "~/constants" @@ -127,8 +126,6 @@ export const MyWalletSection = () => { - - ) } diff --git a/apps/renderer/src/modules/power/ranking/index.tsx b/apps/renderer/src/modules/power/ranking/index.tsx new file mode 100644 index 0000000000..5b04d55924 --- /dev/null +++ b/apps/renderer/src/modules/power/ranking/index.tsx @@ -0,0 +1,126 @@ +import { Fragment } from "react" +import { useTranslation } from "react-i18next" + +import { useWhoami } from "~/atoms/user" +import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar" +import { MotionButtonBase } from "~/components/ui/button" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "~/components/ui/table" +import { EllipsisHorizontalTextWithTooltip } from "~/components/ui/typography" +import { replaceImgUrlIfNeed } from "~/lib/img-proxy" +import { cn } from "~/lib/utils" +import { usePresentUserProfileModal } from "~/modules/profile/hooks" +import { SettingSectionTitle } from "~/modules/settings/section" +import { Balance } from "~/modules/wallet/balance" +import type { useWalletTransactions } from "~/queries/wallet" +import { useWalletRanking } from "~/queries/wallet" + +const medals = ["", "🥇", "🥈", "🥉"] +const rankNumber = (index: number) => { + if (index < medals.length) { + return {medals[index]} + } + return {index} +} + +export const PowerRanking: Component = ({ className }) => { + const { t } = useTranslation("settings") + const ranking = useWalletRanking() + + return ( +
+ +
+ + + + # + {t("wallet.ranking.name")} + {t("wallet.ranking.power")} + {t("wallet.ranking.level")} + + + + {ranking.data?.map((row, index) => { + const lastRow = ranking.data?.[index - 1] + const hasGap = lastRow?.rank && row.rank && row.rank - lastRow.rank !== 1 + + return ( + + {!!hasGap && ( + <> + + ... + + +
+ + + )} + + {!!row.rank && rankNumber(row.rank)} + + + + + {row.powerToken} + + {row.level} + + + ) + })} + + ... + + +
+
+
+ ) +} + +const UserRenderer = ({ + user, +}: { + user?: NonNullable["data"]>[number][ + | "fromUser" + | "toUser"] +}) => { + const { t } = useTranslation("settings") + const me = useWhoami() + const isMe = user?.id === me?.id + + const name = isMe ? t("wallet.transactions.you") : user?.name || APP_NAME + + const presentUserModal = usePresentUserProfileModal("drawer") + return ( + { + if (user?.id) presentUserModal(user.id) + }} + className="flex w-full min-w-0 cursor-button items-center gap-2" + > + + + {name?.slice(0, 2)} + + +
+ + {isMe ? ( + {t("wallet.transactions.you")} + ) : ( + {name} + )} + +
+
+ ) +} diff --git a/apps/renderer/src/pages/(main)/(layer)/(subview)/power/index.tsx b/apps/renderer/src/pages/(main)/(layer)/(subview)/power/index.tsx index e0b33fae5e..d91affa3df 100644 --- a/apps/renderer/src/pages/(main)/(layer)/(subview)/power/index.tsx +++ b/apps/renderer/src/pages/(main)/(layer)/(subview)/power/index.tsx @@ -1,6 +1,8 @@ import { useTranslation } from "react-i18next" +import { Divider } from "~/components/ui/divider" import { MyWalletSection } from "~/modules/power/my-wallet-section" +import { PowerRanking } from "~/modules/power/ranking" import { TransactionsSection } from "~/modules/power/transaction-section" import { useSubViewTitle } from "../hooks" @@ -23,6 +25,10 @@ export function Component() { + + + + ) diff --git a/apps/renderer/src/queries/wallet.tsx b/apps/renderer/src/queries/wallet.tsx index 89d24f9b29..abc1b445d1 100644 --- a/apps/renderer/src/queries/wallet.tsx +++ b/apps/renderer/src/queries/wallet.tsx @@ -42,6 +42,20 @@ export const wallet = { }, ), }, + + ranking: { + get: () => + defineQuery( + ["wallet", "ranking"], + async () => { + const res = await apiClient.wallets.ranking.$get() + return res.data + }, + { + rootKey: ["wallet", "ranking"], + }, + ), + }, } export const useWallet = () => useAuthQuery(wallet.get()) @@ -49,6 +63,8 @@ export const useWallet = () => useAuthQuery(wallet.get()) export const useWalletTransactions = (query: Parameters[0] = {}) => useAuthQuery(wallet.transactions.get(query)) +export const useWalletRanking = () => useAuthQuery(wallet.ranking.get()) + export const useCreateWalletMutation = () => useMutation({ mutationKey: ["createWallet"], diff --git a/locales/settings/en.json b/locales/settings/en.json index c92397535e..ddc9fe6287 100644 --- a/locales/settings/en.json +++ b/locales/settings/en.json @@ -229,6 +229,11 @@ "wallet.create.description": "Create a free wallet to receive Power, which can be used to reward creators and also get rewarded for your content contributions.", "wallet.power.dailyClaim": "You can claim {{amount}} free Power daily, which can be used to tip RSS entries on Follow.", "wallet.power.description": "Power is an ERC-20 token on the {{blockchainName}} blockchain.", + "wallet.ranking.level": "Level", + "wallet.ranking.name": "Name", + "wallet.ranking.power": "Power", + "wallet.ranking.rank": "Rank", + "wallet.ranking.title": "Power Ranking", "wallet.sidebar_title": "Power", "wallet.transactions.amount": "Amount", "wallet.transactions.date": "Date", diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index aa6922d000..e985321a55 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -3923,6 +3923,22 @@ declare const wallets: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; + powerToken: drizzle_orm_pg_core.PgColumn<{ + name: "power_token"; + tableName: "wallets"; + dataType: "string"; + columnType: "PgNumeric"; + data: string; + driverParam: string; + notNull: true; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; dailyPowerToken: drizzle_orm_pg_core.PgColumn<{ name: "daily_power_token"; tableName: "wallets"; @@ -3955,6 +3971,86 @@ declare const wallets: drizzle_orm_pg_core.PgTableWithColumns<{ baseColumn: never; generated: undefined; }, {}, {}>; + rank: drizzle_orm_pg_core.PgColumn<{ + name: "rank"; + tableName: "wallets"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + level: drizzle_orm_pg_core.PgColumn<{ + name: "level"; + tableName: "wallets"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + preActivePoints: drizzle_orm_pg_core.PgColumn<{ + name: "pre_active_points"; + tableName: "wallets"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + activePoints: drizzle_orm_pg_core.PgColumn<{ + name: "active_points"; + tableName: "wallets"; + dataType: "number"; + columnType: "PgInteger"; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; + statusUpdatedAt: drizzle_orm_pg_core.PgColumn<{ + name: "status_updated_at"; + tableName: "wallets"; + dataType: "date"; + columnType: "PgTimestamp"; + data: Date; + driverParam: string; + notNull: false; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + }, {}, {}>; }; dialect: "pg"; }>; @@ -3963,22 +4059,40 @@ declare const walletsOpenAPISchema: zod.ZodObject<{ address: zod.ZodNullable; userId: zod.ZodString; createdAt: zod.ZodString; + powerToken: zod.ZodString; dailyPowerToken: zod.ZodString; cashablePowerToken: zod.ZodString; + rank: zod.ZodNullable; + level: zod.ZodNullable; + preActivePoints: zod.ZodNullable; + activePoints: zod.ZodNullable; + statusUpdatedAt: zod.ZodNullable; }, zod.UnknownKeysParam, zod.ZodTypeAny, { createdAt: string; userId: string; addressIndex: number; address: string | null; + powerToken: string; dailyPowerToken: string; cashablePowerToken: string; + rank: number | null; + level: number | null; + preActivePoints: number | null; + activePoints: number | null; + statusUpdatedAt: string | null; }, { createdAt: string; userId: string; addressIndex: number; address: string | null; + powerToken: string; dailyPowerToken: string; cashablePowerToken: string; + rank: number | null; + level: number | null; + preActivePoints: number | null; + activePoints: number | null; + statusUpdatedAt: string | null; }>; declare const walletsRelations: drizzle_orm.Relations<"wallets", { user: drizzle_orm.One<"user", true>; @@ -4170,10 +4284,10 @@ declare const transactionsOpenAPISchema: zod.ZodObject<{ fromUserId: string | null; toUserId: string | null; hash: string; + powerToken: string; toFeedId: string | null; toListId: string | null; toEntryId: string | null; - powerToken: string; comment: string | null; }, { type: "tip" | "mint" | "burn" | "withdraw" | "purchase"; @@ -4181,10 +4295,10 @@ declare const transactionsOpenAPISchema: zod.ZodObject<{ fromUserId: string | null; toUserId: string | null; hash: string; + powerToken: string; toFeedId: string | null; toListId: string | null; toEntryId: string | null; - powerToken: string; comment: string | null; }>; declare const transactionsRelations: drizzle_orm.Relations<"transactions", { @@ -4783,10 +4897,10 @@ declare const _routes: hono_hono_base.HonoBase