feat: replace number formatting with formatNumber utility across discover and feed components for improved readability

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-06-06 14:15:07 +08:00
parent fc16118891
commit 5a53e6f46e
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
13 changed files with 63 additions and 32 deletions

View File

@ -2,9 +2,9 @@ import { Button } from "@follow/components/ui/button/index.js"
import { Card, CardContent, CardHeader } from "@follow/components/ui/card/index.jsx"
import { RelativeTime } from "@follow/components/ui/datetime/index.js"
import { getBackgroundGradient } from "@follow/utils/color"
import { cn } from "@follow/utils/utils"
import { cn, formatNumber } from "@follow/utils/utils"
import type { FC } from "react"
import { memo, useMemo } from "react"
import { memo } from "react"
import { useTranslation } from "react-i18next"
import { useLocation } from "react-router"
@ -95,7 +95,6 @@ interface DiscoverFeedCardProps {
export const DiscoverFeedCard: FC<DiscoverFeedCardProps> = memo(
({ item, onSuccess, className }) => {
const numberFormatter = useMemo(() => new Intl.NumberFormat("en-US", {}), [])
const { t } = useTranslation("common")
const isSubscribed = useIsSubscribed(item.feed?.id || item.list?.id || "")
@ -140,7 +139,7 @@ export const DiscoverFeedCard: FC<DiscoverFeedCardProps> = memo(
<i className="i-mgc-user-3-cute-re" />
<span>
{numberFormatter.format(item.analytics.subscriptionCount)}{" "}
{formatNumber(item.analytics.subscriptionCount)}{" "}
{t("feed.follower", { count: item.analytics.subscriptionCount })}
</span>
</div>

View File

@ -1,7 +1,7 @@
import { Skeleton } from "@follow/components/ui/skeleton/index.js"
import type { FeedAnalyticsModel, FeedOrListRespModel, ListAnalyticsModel } from "@follow/models"
import { formatNumber } from "@follow/utils"
import type { FC } from "react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { RelativeTime } from "~/components/ui/datetime"
@ -17,7 +17,7 @@ export interface FeedSummaryProps {
}
export const FeedSummary: FC<FeedSummaryProps> = ({ feed, analytics, showAnalytics = true }) => {
const { t } = useTranslation("common")
const numberFormatter = useMemo(() => new Intl.NumberFormat("en-US", {}), [])
return (
<div>
<FollowSummary feed={feed} />
@ -33,7 +33,7 @@ export const FeedSummary: FC<FeedSummaryProps> = ({ feed, analytics, showAnalyti
<i className="i-mgc-user-3-cute-re" />
<span>
{numberFormatter.format(analytics.subscriptionCount)}{" "}
{formatNumber(analytics.subscriptionCount)}{" "}
{t("feed.follower", { count: analytics.subscriptionCount })}
</span>
</div>

View File

@ -1,5 +1,5 @@
import { formatNumber } from "@follow/utils"
import type { FC } from "react"
import { useMemo } from "react"
import { useTranslation } from "react-i18next"
import { useIsSubscribed } from "~/store/subscription"
@ -13,7 +13,6 @@ export const TrendingFeedCard: FC<{
}> = ({ item }) => {
const { t } = useTranslation("common")
const { analytics } = item
const numberFormatter = useMemo(() => new Intl.NumberFormat("en-US", {}), [])
const isSubscribed = useIsSubscribed(item.feed?.id || item.list?.id || "")
return (
<div>
@ -25,7 +24,7 @@ export const TrendingFeedCard: FC<{
<i className="i-mgc-user-3-cute-re" />
<span>
{numberFormatter.format(analytics.subscriptionCount)}{" "}
{formatNumber(analytics.subscriptionCount)}{" "}
{t("feed.follower", { count: analytics.subscriptionCount })}
</span>
</div>

View File

@ -23,7 +23,7 @@ import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typogra
import { views } from "@follow/constants"
import type { ExtractBizResponse } from "@follow/models"
import { jotaiStore } from "@follow/utils"
import { sortByAlphabet } from "@follow/utils/utils"
import { formatNumber, sortByAlphabet } from "@follow/utils/utils"
import clsx from "clsx"
import { useSingleton } from "foxact/use-singleton"
import type { PrimitiveAtom } from "jotai"
@ -140,7 +140,7 @@ const SubscriptionFeedsSection = () => {
return nextSet
})
})
scrollContainerElement.querySelectorAll("button[data-id]").forEach((el) => {
scrollContainerElement.querySelectorAll("[data-id]").forEach((el) => {
observer.observe(el)
})
return () => {
@ -506,13 +506,13 @@ const FeedListItem = memo(
<div className="text-center text-xs">
{analytics ? (
<div className="flex flex-col gap-1">
<div className="flex items-center justify-center gap-3">
<div className="grid grid-cols-2 gap-1">
<Tooltip>
<TooltipTrigger asChild>
<div className="text-text-secondary flex items-center gap-1">
<i className="i-mgc-user-3-cute-re" />
<span className="tabular-nums">
{analytics.subscriptionCount?.toLocaleString() || "0"}
{formatNumber(analytics.subscriptionCount || 0)}
</span>
</div>
</TooltipTrigger>
@ -525,8 +525,8 @@ const FeedListItem = memo(
<div className="text-text-secondary flex items-center gap-1">
<i className="i-mgc-safety-certificate-cute-re" />
<span className="tabular-nums">
{analytics.updatesPerWeek?.toFixed(1) || "0"}
{"/week"}
{Math.round(analytics.updatesPerWeek || 0) || "0"}
{"/w"}
</span>
</div>
</TooltipTrigger>

View File

@ -18,7 +18,7 @@ import {
TooltipTrigger,
} from "@follow/components/ui/tooltip/index.jsx"
import { views } from "@follow/constants"
import { cn } from "@follow/utils/utils"
import { cn, formatNumber } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
import { useMemo } from "react"
import { toast } from "sonner"
@ -175,7 +175,7 @@ export const SettingLists = () => {
</div>
</TableCell>
<TableCell size="sm" className="tabular-nums">
{listDataMap[row.id]?.subscriptionCount}
{formatNumber(listDataMap[row.id]?.subscriptionCount || 0)}
</TableCell>
<TableCell size="sm" className="tabular-nums">
<Balance>{BigInt(listDataMap[row.id]?.purchaseAmount || 0n)}</Balance>

View File

@ -4,7 +4,7 @@ import { Input } from "@follow/components/ui/input/Input.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.js"
import { EllipsisHorizontalTextWithTooltip } from "@follow/components/ui/typography/EllipsisWithTooltip.js"
import { CategoryMap, RSSHubCategories } from "@follow/constants"
import { cn } from "@follow/utils/utils"
import { cn, formatNumber } from "@follow/utils/utils"
import { keepPreviousData } from "@tanstack/react-query"
import { useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
@ -281,7 +281,7 @@ const RecommendationListItem = ({
{analytics?.subscriptionCount && (
<div className="flex items-center gap-0.5 text-xs">
<i className="i-mgc-fire-cute-re" />
{analytics?.subscriptionCount}
{formatNumber(analytics?.subscriptionCount || 0)}
</div>
)}
</div>

View File

@ -1,4 +1,4 @@
import { cn } from "@follow/utils"
import { cn, formatNumber } from "@follow/utils"
import { useQuery } from "@tanstack/react-query"
import { Text, View } from "react-native"
import { useColor } from "react-native-uikit-colors"
@ -87,7 +87,9 @@ export const Trending = ({
>
<View className="flex flex-row items-center gap-1 opacity-60">
<User3CuteReIcon width={13} height={13} />
<Text className="text-text text-sm">{item.analytics.subscriptionCount}</Text>
<Text className="text-text text-sm">
{formatNumber(item.analytics.subscriptionCount || 0)}
</Text>
</View>
</FeedSummary>
))

View File

@ -1,4 +1,5 @@
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
import { formatNumber } from "@follow/utils"
import { Text, View } from "react-native"
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
@ -22,7 +23,7 @@ export const SearchFeedCard = ({ item }: { item: SearchResultItem }) => {
<View className="flex-row items-center gap-1.5">
<User3CuteReIcon width={14} height={14} color={iconColor} />
<Text className="text-secondary-label text-sm">
{item.analytics?.subscriptionCount} followers
{formatNumber(item.analytics?.subscriptionCount || 0)} followers
</Text>
</View>
<View className="flex-row items-center gap-1.5">

View File

@ -1,4 +1,5 @@
import { useSubscriptionByListId } from "@follow/store/subscription/hooks"
import { formatNumber } from "@follow/utils"
import { useQuery } from "@tanstack/react-query"
import { useAtomValue } from "jotai"
import { memo } from "react"
@ -124,7 +125,9 @@ const SearchListCard = memo(({ item }: { item: SearchResultItem }) => {
<View className="mt-4 flex-row items-center gap-6 pl-4 opacity-60">
<View className="flex-row items-center gap-2">
<User3CuteReIcon width={16} height={16} color={iconColor} />
<Text className="text-text">{item.subscriptionCount} followers</Text>
<Text className="text-text">
{formatNumber(item.analytics?.subscriptionCount || 0)} followers
</Text>
</View>
</View>
</ItemPressable>

View File

@ -3,6 +3,7 @@ import { useFeed, usePrefetchFeed, usePrefetchFeedByUrl } from "@follow/store/fe
import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks"
import { subscriptionSyncService } from "@follow/store/subscription/store"
import type { SubscriptionForm } from "@follow/store/subscription/types"
import { formatNumber } from "@follow/utils"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { Controller, useForm } from "react-hook-form"
@ -184,7 +185,7 @@ function FollowImpl(props: { feedId: string }) {
<View className="flex-row items-center gap-1">
<User3CuteReIcon width={12} height={12} />
<Text className="text-text text-sm">
{feed.subscriptionCount}{" "}
{formatNumber(feed.subscriptionCount || 0)}{" "}
{tCommon("feed.follower", { count: feed.subscriptionCount })}
</Text>
</View>

View File

@ -10,13 +10,11 @@ import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avata
import { Button } from "@follow/components/ui/button/index.jsx"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { useTitle } from "@follow/hooks"
import { cn } from "@follow/utils/utils"
import { cn, formatNumber } from "@follow/utils/utils"
import { Fragment, memo } from "react"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router"
const numberFormatter = new Intl.NumberFormat()
const FeedRow = memo<{ feed: Feed["feed"] }>(({ feed }) => {
return (
<a
@ -154,7 +152,7 @@ export function Component() {
{!!list.data?.subscriptionCount && (
<div className="px-4 text-center">
<div className="text-xl font-semibold text-zinc-900 dark:text-zinc-100">
{numberFormatter.format(list.data.subscriptionCount)}
{formatNumber(list.data.subscriptionCount)}
</div>
<div className="text-sm text-zinc-500 dark:text-zinc-400">
{t("feed.follower", { count: list.data.subscriptionCount })}

View File

@ -1,4 +1,5 @@
import { getFeedIconSrc } from "@follow/components/utils/icon.js"
import { formatNumber } from "@follow/utils"
import * as React from "react"
import type { ApiClient } from "~/lib/api-client"
@ -24,7 +25,7 @@ export const renderFeedOG = async (apiClient: ApiClient, feedId: string) => {
fallback: true,
src: image!,
})
const numberFormatter = new Intl.NumberFormat("en-US")
const imageBase64 = await getImageBase64(image || src)
try {
@ -84,8 +85,8 @@ export const renderFeedOG = async (apiClient: ApiClient, feedId: string) => {
fontWeight: 500,
}}
>
{numberFormatter.format(feed.data.subscriptionCount)} followers with{" "}
{numberFormatter.format(feed.data.readCount)} recent reads on Folo
{formatNumber(feed.data.subscriptionCount || 0)} followers with{" "}
{formatNumber(feed.data.readCount || 0)} recent reads on Folo
</p>
</div>
</OGCanvas>,

View File

@ -449,3 +449,30 @@ export function doesTextContainHTML(text?: string | null): boolean {
if (!text) return false
return /<([a-z][a-z0-9]*)\b[^>]*>\s*[^<>\s].*<\/\1>/i.test(text)
}
/**
* Format number to a more readable format
* @param num - The number to format
* @returns The formatted number
*/
export function formatNumber(num: number): string {
// Handle negative numbers
const isNegative = num < 0
const absNum = Math.abs(num)
// Define thresholds
const billion = 1_000_000_000
const million = 1_000_000
const thousand = 1_000
// Format based on number size
if (absNum >= billion) {
return `${isNegative ? "-" : ""}${(absNum / billion).toFixed(1)}B`
} else if (absNum >= million) {
return `${isNegative ? "-" : ""}${(absNum / million).toFixed(1)}M`
} else if (absNum >= thousand) {
return `${isNegative ? "-" : ""}${(absNum / thousand).toFixed(1)}K`
}
return `${isNegative ? "-" : ""}${absNum}`
}