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:
parent
fc16118891
commit
5a53e6f46e
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue